bdsmgdjfv commited on
Commit
10192e5
Β·
verified Β·
1 Parent(s): b8cd6e9

Upload 14 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for AnyCoder Docker Space
2
+
3
+ # Stage 1: Build frontend
4
+ FROM node:22-slim AS frontend-builder
5
+
6
+ WORKDIR /build
7
+
8
+ # Copy frontend package files
9
+ COPY frontend/package*.json ./
10
+ RUN npm ci
11
+
12
+ # Copy all frontend source files and configs
13
+ COPY frontend/src ./src
14
+ COPY frontend/public ./public
15
+ COPY frontend/next.config.js ./
16
+ COPY frontend/tsconfig.json ./
17
+ COPY frontend/tailwind.config.js ./
18
+ COPY frontend/postcss.config.js ./
19
+ # Note: next-env.d.ts is auto-generated by Next.js, not needed for build
20
+
21
+ # Build frontend
22
+ RUN npm run build
23
+
24
+ # Stage 2: Production image
25
+ FROM python:3.11-slim
26
+
27
+ # Install system dependencies as root (git for pip, nodejs for frontend)
28
+ # Install Node.js 22 from NodeSource (Debian repo only has v18)
29
+ RUN apt-get update && \
30
+ apt-get install -y --no-install-recommends curl ca-certificates gnupg git && \
31
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
32
+ apt-get install -y --no-install-recommends nodejs && \
33
+ rm -rf /var/lib/apt/lists/*
34
+
35
+ # Set up a new user named "user" with user ID 1000
36
+ RUN useradd -m -u 1000 user
37
+
38
+ # Switch to the "user" user
39
+ USER user
40
+
41
+ # Set home to the user's home directory
42
+ ENV HOME=/home/user \
43
+ PATH=/home/user/.local/bin:$PATH \
44
+ PYTHONUNBUFFERED=1
45
+
46
+ # Set the working directory to the user's home directory
47
+ WORKDIR $HOME/app
48
+
49
+ # Copy Python requirements and install dependencies
50
+ COPY --chown=user:user requirements.txt .
51
+ RUN pip install --no-cache-dir --upgrade pip && \
52
+ pip install --no-cache-dir -r requirements.txt
53
+
54
+ # Copy application code
55
+ COPY --chown=user:user backend_api.py .
56
+ COPY --chown=user:user backend_models.py .
57
+ COPY --chown=user:user backend_docs_manager.py .
58
+ COPY --chown=user:user backend_prompts.py .
59
+ COPY --chown=user:user backend_parsers.py .
60
+ COPY --chown=user:user backend_deploy.py .
61
+ COPY --chown=user:user backend_search_replace.py .
62
+ COPY --chown=user:user project_importer.py .
63
+
64
+ # Copy built frontend from builder stage
65
+ COPY --chown=user:user --from=frontend-builder /build/.next ./frontend/.next
66
+ COPY --chown=user:user --from=frontend-builder /build/public ./frontend/public
67
+ COPY --chown=user:user --from=frontend-builder /build/package*.json ./frontend/
68
+ COPY --chown=user:user --from=frontend-builder /build/next.config.js ./frontend/
69
+ COPY --chown=user:user --from=frontend-builder /build/node_modules ./frontend/node_modules
70
+
71
+ # Set environment variables for the application
72
+ # BACKEND_HOST is used by Next.js server for proxying
73
+ # Do NOT set NEXT_PUBLIC_API_URL - let frontend use relative URLs
74
+ ENV BACKEND_HOST=http://localhost:8000 \
75
+ PORT=7860
76
+
77
+ # Create startup script that runs both services
78
+ # Backend on 8000, Frontend on 7860 (exposed port)
79
+ RUN echo '#!/bin/bash\n\
80
+ set -e\n\
81
+ \n\
82
+ echo "πŸš€ Starting AnyCoder Docker Space..."\n\
83
+ \n\
84
+ # Start backend on port 8000 in background\n\
85
+ echo "πŸ“‘ Starting FastAPI backend on port 8000..."\n\
86
+ cd $HOME/app\n\
87
+ uvicorn backend_api:app --host 0.0.0.0 --port 8000 &\n\
88
+ BACKEND_PID=$!\n\
89
+ \n\
90
+ # Wait for backend to be ready\n\
91
+ echo "⏳ Waiting for backend to start..."\n\
92
+ sleep 5\n\
93
+ \n\
94
+ # Start frontend on port 7860 (HF Spaces exposed port)\n\
95
+ echo "🎨 Starting Next.js frontend on port 7860..."\n\
96
+ cd $HOME/app/frontend\n\
97
+ PORT=7860 BACKEND_HOST=http://localhost:8000 npm start\n\
98
+ ' > $HOME/app/start.sh && chmod +x $HOME/app/start.sh
99
+
100
+ # Expose port 7860 (HF Spaces default)
101
+ EXPOSE 7860
102
+
103
+ # Run the startup script
104
+ CMD ["./start.sh"]
105
+
README.md ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AnyCoder
3
+ emoji: πŸ†
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ disable_embedding: false
10
+ hf_oauth: true
11
+ hf_oauth_expiration_minutes: 43200
12
+ hf_oauth_scopes:
13
+ - manage-repos
14
+ - write-discussions
15
+ ---
16
+
17
+
18
+ # AnyCoder - AI Code Generator with React Frontend
19
+
20
+ AnyCoder is a full-stack AI-powered code generator with a modern React/TypeScript frontend and FastAPI backend. Generate applications by describing them in plain English, with support for multiple AI models and one-click deployment to Hugging Face Spaces.
21
+
22
+ ## 🎨 Features
23
+
24
+ - **Modern React UI**: Apple-inspired design with VS Code layout
25
+ - **Real-time Streaming**: Server-Sent Events for live code generation
26
+ - **Multi-Model Support**: MiniMax M2, DeepSeek V3, and more via HuggingFace InferenceClient
27
+ - **Multiple Languages**: HTML, Gradio, Streamlit, React, Transformers.js, ComfyUI
28
+ - **Authentication**: HuggingFace OAuth + Dev mode for local testing
29
+ - **One-Click Deployment**: Deploy generated apps directly to HF Spaces
30
+
31
+ ## πŸ—οΈ Architecture
32
+
33
+ ```
34
+ anycoder/
35
+ β”œβ”€β”€ backend_api.py # FastAPI backend with streaming
36
+ β”œβ”€β”€ frontend/ # Next.js React frontend
37
+ β”‚ β”œβ”€β”€ src/
38
+ β”‚ β”‚ β”œβ”€β”€ app/ # Pages (page.tsx, layout.tsx, globals.css)
39
+ β”‚ β”‚ β”œβ”€β”€ components/ # React components
40
+ β”‚ β”‚ β”œβ”€β”€ lib/ # API client, auth utilities
41
+ β”‚ β”‚ └── types/ # TypeScript types
42
+ β”‚ └── package.json
43
+ β”œβ”€β”€ requirements.txt # Python dependencies
44
+ β”œβ”€β”€ Dockerfile # Docker Space configuration
45
+ └── start_fullstack.sh # Local development script
46
+ ```
47
+
48
+ ## πŸš€ Quick Start
49
+
50
+ ### Local Development
51
+
52
+ 1. **Backend**:
53
+ ```bash
54
+ export HF_TOKEN="your_huggingface_token"
55
+ export GEMINI_API_KEY="your_gemini_api_key"
56
+ python backend_api.py
57
+ ```
58
+
59
+ 2. **Frontend** (new terminal):
60
+ ```bash
61
+ cd frontend
62
+ npm install
63
+ npm run dev
64
+ ```
65
+
66
+ 3. Open `http://localhost:3000`
67
+
68
+ ### Using start script:
69
+ ```bash
70
+ export HF_TOKEN="your_token"
71
+ export GEMINI_API_KEY="your_gemini_api_key"
72
+ ./start_fullstack.sh
73
+ ```
74
+
75
+ ## 🐳 Docker Space Deployment
76
+
77
+ This app runs as a Docker Space on HuggingFace. The Dockerfile:
78
+ - Builds the Next.js frontend
79
+ - Runs FastAPI backend on port 7860
80
+ - Uses proper user permissions (UID 1000)
81
+ - Handles environment variables securely
82
+
83
+ ## πŸ”‘ Authentication
84
+
85
+ - **Dev Mode** (localhost): Mock login for testing
86
+ - **Production**: HuggingFace OAuth with manage-repos scope
87
+
88
+ ## πŸ“ Supported Languages
89
+
90
+ - `html` - Static HTML pages
91
+ - `gradio` - Python Gradio apps
92
+ - `streamlit` - Python Streamlit apps
93
+ - `react` - React/Next.js apps
94
+ - `transformers.js` - Browser ML apps
95
+ - `comfyui` - ComfyUI workflows
96
+
97
+ ## πŸ€– Available Models
98
+
99
+ - **Gemini 3 Pro Preview** (Default) - Google's latest with deep thinking & Google Search
100
+ - MiniMax M2 (via HF router with Novita)
101
+ - DeepSeek V3/V3.1
102
+ - DeepSeek R1
103
+ - And more via HuggingFace InferenceClient
104
+
105
+ ## 🎯 Usage
106
+
107
+ 1. Sign in with HuggingFace (or use Dev Login locally)
108
+ 2. Select a language and AI model
109
+ 3. Describe your app in the chat
110
+ 4. Watch code generate in real-time
111
+ 5. Click **πŸš€ Deploy** to publish to HF Spaces
112
+
113
+ ## πŸ› οΈ Environment Variables
114
+
115
+ - `HF_TOKEN` - HuggingFace API token (required)
116
+ - `GEMINI_API_KEY` - Google Gemini API key (required for Gemini 3 Pro Preview)
117
+ - `POE_API_KEY` - Poe API key (optional, for GPT-5 and Claude models)
118
+ - `DASHSCOPE_API_KEY` - DashScope API key (optional, for Qwen models)
119
+ - `OPENROUTER_API_KEY` - OpenRouter API key (optional, for Sherlock models)
120
+ - `MISTRAL_API_KEY` - Mistral API key (optional, for Mistral models)
121
+
122
+ ## πŸ“¦ Tech Stack
123
+
124
+ **Frontend:**
125
+ - Next.js 14
126
+ - TypeScript
127
+ - Tailwind CSS
128
+ - Monaco Editor
129
+
130
+ **Backend:**
131
+ - FastAPI
132
+ - HuggingFace Hub
133
+ - Server-Sent Events (SSE)
134
+
135
+ ## πŸ“„ License
136
+
137
+ MIT
backend_api.py ADDED
@@ -0,0 +1,1731 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI backend for AnyCoder - provides REST API endpoints
3
+ """
4
+ from fastapi import FastAPI, HTTPException, Header, WebSocket, WebSocketDisconnect, Request, Response
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import StreamingResponse, RedirectResponse, JSONResponse
7
+ from pydantic import BaseModel
8
+ from typing import Optional, List, Dict, AsyncGenerator
9
+ import json
10
+ import asyncio
11
+ from datetime import datetime, timedelta
12
+ import secrets
13
+ import base64
14
+ import urllib.parse
15
+ import re
16
+
17
+ # Import only what we need, avoiding Gradio UI imports
18
+ import sys
19
+ import os
20
+ from huggingface_hub import InferenceClient
21
+ import httpx
22
+
23
+ # Import model handling from backend_models
24
+ from backend_models import (
25
+ get_inference_client,
26
+ get_real_model_id,
27
+ is_native_sdk_model,
28
+ is_mistral_model
29
+ )
30
+
31
+ # Import project importer for importing from HF/GitHub
32
+ from project_importer import ProjectImporter
33
+
34
+ # Import system prompts from standalone backend_prompts.py
35
+ # No dependencies on Gradio or heavy libraries
36
+ print("[Startup] Loading system prompts from backend_prompts...")
37
+
38
+ try:
39
+ from backend_prompts import (
40
+ HTML_SYSTEM_PROMPT,
41
+ TRANSFORMERS_JS_SYSTEM_PROMPT,
42
+ STREAMLIT_SYSTEM_PROMPT,
43
+ REACT_SYSTEM_PROMPT,
44
+ REACT_FOLLOW_UP_SYSTEM_PROMPT, # Import React followup prompt
45
+ get_gradio_system_prompt, # Import the function to get dynamic prompt
46
+ get_comfyui_system_prompt, # Import the function to get dynamic ComfyUI prompt
47
+ JSON_SYSTEM_PROMPT,
48
+ DAGGR_SYSTEM_PROMPT,
49
+ GENERIC_SYSTEM_PROMPT
50
+ )
51
+ # Get the Gradio system prompt (includes full Gradio 6 documentation)
52
+ GRADIO_SYSTEM_PROMPT = get_gradio_system_prompt()
53
+ # Get the ComfyUI system prompt (includes full ComfyUI documentation)
54
+ COMFYUI_SYSTEM_PROMPT = get_comfyui_system_prompt()
55
+ print("[Startup] βœ… All system prompts loaded successfully from backend_prompts.py")
56
+ print(f"[Startup] πŸ“š Gradio system prompt loaded with full documentation ({len(GRADIO_SYSTEM_PROMPT)} chars)")
57
+ print(f"[Startup] πŸ“š ComfyUI system prompt loaded with full documentation ({len(COMFYUI_SYSTEM_PROMPT)} chars)")
58
+ except Exception as e:
59
+ import traceback
60
+ print(f"[Startup] ❌ ERROR: Could not import from backend_prompts: {e}")
61
+ print(f"[Startup] Traceback: {traceback.format_exc()}")
62
+ print("[Startup] Using minimal fallback prompts")
63
+
64
+ # Define minimal fallback prompts
65
+ HTML_SYSTEM_PROMPT = "You are an expert web developer. Create complete HTML applications with CSS and JavaScript."
66
+ TRANSFORMERS_JS_SYSTEM_PROMPT = "You are an expert at creating transformers.js applications. Generate complete working code."
67
+ STREAMLIT_SYSTEM_PROMPT = "You are an expert Streamlit developer. Create complete Streamlit applications."
68
+ REACT_SYSTEM_PROMPT = "You are an expert React developer. Create complete React applications with Next.js."
69
+ GRADIO_SYSTEM_PROMPT = "You are an expert Gradio developer. Create complete, working Gradio applications."
70
+ COMFYUI_SYSTEM_PROMPT = "You are an expert ComfyUI developer. Generate clean, valid JSON workflows for ComfyUI based on the user's request. READ THE USER'S REQUEST CAREFULLY and create a workflow that matches their specific needs."
71
+ JSON_SYSTEM_PROMPT = "You are an expert at generating JSON configurations. Create valid, well-structured JSON."
72
+ GENERIC_SYSTEM_PROMPT = "You are an expert {language} developer. Create complete, working {language} applications."
73
+
74
+ print("[Startup] System prompts initialization complete")
75
+
76
+ # Cache system prompts map for fast lookup (created once at startup)
77
+ SYSTEM_PROMPT_CACHE = {
78
+ "html": HTML_SYSTEM_PROMPT,
79
+ "gradio": GRADIO_SYSTEM_PROMPT,
80
+ "streamlit": STREAMLIT_SYSTEM_PROMPT,
81
+ "transformers.js": TRANSFORMERS_JS_SYSTEM_PROMPT,
82
+ "react": REACT_SYSTEM_PROMPT,
83
+ "comfyui": COMFYUI_SYSTEM_PROMPT, # Use ComfyUI-specific prompt with documentation
84
+ "daggr": DAGGR_SYSTEM_PROMPT,
85
+ }
86
+
87
+ # Client connection pool for reuse (thread-safe)
88
+ import threading
89
+ _client_pool = {}
90
+ _client_pool_lock = threading.Lock()
91
+
92
+ def get_cached_client(model_id: str, provider: str = "auto"):
93
+ """Get or create a cached API client for reuse"""
94
+ cache_key = f"{model_id}:{provider}"
95
+
96
+ with _client_pool_lock:
97
+ if cache_key not in _client_pool:
98
+ _client_pool[cache_key] = get_inference_client(model_id, provider)
99
+ return _client_pool[cache_key]
100
+
101
+ # Define models and languages here to avoid importing Gradio UI
102
+ AVAILABLE_MODELS = [
103
+ {"name": "Kimi-K2.5 🧠", "id": "moonshotai/Kimi-K2.5", "description": "Kimi-K2.5 - New powerful reasoning model via HuggingFace Router with Novita provider (Default)", "supports_images": True},
104
+ {"name": "GLM-4.7-Flash ⚑", "id": "zai-org/GLM-4.7-Flash", "description": "GLM-4.7-Flash - Ultra-fast GLM model via HuggingFace Router with Novita provider", "supports_images": False},
105
+ {"name": "GLM-4.7", "id": "zai-org/GLM-4.7", "description": "GLM-4.7 - Latest GLM model via HuggingFace Router with Cerebras provider", "supports_images": False},
106
+ {"name": "MiniMax M2.1", "id": "MiniMaxAI/MiniMax-M2.1", "description": "MiniMax M2.1 - Enhanced model via HuggingFace Router", "supports_images": False},
107
+ {"name": "GLM-4.6", "id": "zai-org/GLM-4.6", "description": "GLM-4.6 model via HuggingFace with Cerebras provider", "supports_images": False},
108
+ {"name": "GLM-4.6V πŸ‘οΈ", "id": "zai-org/GLM-4.6V:zai-org", "description": "GLM-4.6V vision model - supports image uploads for visual understanding", "supports_images": True},
109
+ {"name": "DeepSeek V3", "id": "deepseek-ai/DeepSeek-V3", "description": "DeepSeek V3 - Fast model for code generation via HuggingFace Router with Novita provider", "supports_images": False},
110
+ {"name": "DeepSeek R1", "id": "deepseek-ai/DeepSeek-R1", "description": "DeepSeek R1 model for code generation via HuggingFace", "supports_images": False},
111
+ {"name": "MiniMax M2", "id": "MiniMaxAI/MiniMax-M2", "description": "MiniMax M2 model via HuggingFace InferenceClient with Novita provider", "supports_images": False},
112
+ {"name": "Kimi K2 Thinking", "id": "moonshotai/Kimi-K2-Thinking", "description": "Moonshot Kimi K2 Thinking model via HuggingFace with Together AI provider", "supports_images": False},
113
+ ]
114
+
115
+ # Cache model lookup for faster access (built after AVAILABLE_MODELS is defined)
116
+ MODEL_CACHE = {model["id"]: model for model in AVAILABLE_MODELS}
117
+ print(f"[Startup] βœ… Performance optimizations loaded: {len(SYSTEM_PROMPT_CACHE)} cached prompts, {len(MODEL_CACHE)} cached models, client pooling enabled")
118
+
119
+ LANGUAGE_CHOICES = ["html", "gradio", "transformers.js", "streamlit", "comfyui", "react", "daggr"]
120
+
121
+ app = FastAPI(title="AnyCoder API", version="1.0.0")
122
+
123
+ # OAuth and environment configuration (must be before CORS)
124
+ OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID", "")
125
+ OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET", "")
126
+ OAUTH_SCOPES = os.getenv("OAUTH_SCOPES", "openid profile manage-repos write-discussions")
127
+ OPENID_PROVIDER_URL = os.getenv("OPENID_PROVIDER_URL", "https://huggingface.co")
128
+ SPACE_HOST = os.getenv("SPACE_HOST", "localhost:7860")
129
+
130
+ # Configure CORS - allow all origins in production, specific in dev
131
+ # In Docker Space, requests come from the same domain via Next.js proxy
132
+ ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",") if os.getenv("ALLOWED_ORIGINS") else [
133
+ "http://localhost:3000",
134
+ "http://localhost:3001",
135
+ "http://localhost:7860",
136
+ f"https://{SPACE_HOST}" if SPACE_HOST and not SPACE_HOST.startswith("localhost") else "http://localhost:7860"
137
+ ]
138
+
139
+ app.add_middleware(
140
+ CORSMiddleware,
141
+ allow_origins=ALLOWED_ORIGINS if ALLOWED_ORIGINS != ["*"] else ["*"],
142
+ allow_credentials=True,
143
+ allow_methods=["*"],
144
+ allow_headers=["*"],
145
+ allow_origin_regex=r"https://.*\.hf\.space" if SPACE_HOST and not SPACE_HOST.startswith("localhost") else None,
146
+ )
147
+
148
+ # In-memory store for OAuth states (in production, use Redis or similar)
149
+ oauth_states = {}
150
+
151
+ # In-memory store for user sessions
152
+ user_sessions = {}
153
+
154
+
155
+ def is_session_expired(session_data: dict) -> bool:
156
+ """Check if session has expired"""
157
+ expires_at = session_data.get("expires_at")
158
+ if not expires_at:
159
+ # If no expiration info, check if session is older than 8 hours
160
+ timestamp = session_data.get("timestamp", datetime.now())
161
+ return (datetime.now() - timestamp) > timedelta(hours=8)
162
+
163
+ return datetime.now() >= expires_at
164
+
165
+
166
+ # Background task for cleaning up expired sessions
167
+ async def cleanup_expired_sessions():
168
+ """Periodically clean up expired sessions"""
169
+ while True:
170
+ try:
171
+ await asyncio.sleep(3600) # Run every hour
172
+
173
+ expired_sessions = []
174
+ for session_token, session_data in user_sessions.items():
175
+ if is_session_expired(session_data):
176
+ expired_sessions.append(session_token)
177
+
178
+ for session_token in expired_sessions:
179
+ user_sessions.pop(session_token, None)
180
+ print(f"[Auth] Cleaned up expired session: {session_token[:10]}...")
181
+
182
+ if expired_sessions:
183
+ print(f"[Auth] Cleaned up {len(expired_sessions)} expired session(s)")
184
+ except Exception as e:
185
+ print(f"[Auth] Cleanup error: {e}")
186
+
187
+ # Start cleanup task on app startup
188
+ @app.on_event("startup")
189
+ async def startup_event():
190
+ """Run startup tasks"""
191
+ asyncio.create_task(cleanup_expired_sessions())
192
+ print("[Startup] βœ… Session cleanup task started")
193
+
194
+
195
+ # Pydantic models for request/response
196
+ class CodeGenerationRequest(BaseModel):
197
+ query: str
198
+ language: str = "html"
199
+ model_id: str = "moonshotai/Kimi-K2.5"
200
+ provider: str = "auto"
201
+ history: List[List[str]] = []
202
+ agent_mode: bool = False
203
+ existing_repo_id: Optional[str] = None # For auto-deploy to update existing space
204
+ skip_auto_deploy: bool = False # Skip auto-deploy (for PR creation)
205
+ image_url: Optional[str] = None # For vision models like GLM-4.6V
206
+
207
+
208
+ class DeploymentRequest(BaseModel):
209
+ code: str
210
+ space_name: Optional[str] = None
211
+ language: str
212
+ requirements: Optional[str] = None
213
+ existing_repo_id: Optional[str] = None # For updating existing spaces
214
+ commit_message: Optional[str] = None
215
+ history: List[Dict] = [] # Chat history for tracking deployed spaces
216
+
217
+
218
+ class AuthStatus(BaseModel):
219
+ authenticated: bool
220
+ username: Optional[str] = None
221
+ message: str
222
+
223
+
224
+ class ModelInfo(BaseModel):
225
+ name: str
226
+ id: str
227
+ description: str
228
+
229
+
230
+ class CodeGenerationResponse(BaseModel):
231
+ code: str
232
+ history: List[List[str]]
233
+ status: str
234
+
235
+
236
+ class ImportRequest(BaseModel):
237
+ url: str
238
+ prefer_local: bool = False
239
+ username: Optional[str] = None # Username of authenticated user for ownership check
240
+
241
+
242
+ class ImportResponse(BaseModel):
243
+ status: str
244
+ message: str
245
+ code: str
246
+ language: str
247
+ url: str
248
+ metadata: Dict
249
+ owned_by_user: bool = False # True if user owns the imported repo
250
+ repo_id: Optional[str] = None # The repo ID (username/repo-name) if applicable
251
+
252
+
253
+ class PullRequestRequest(BaseModel):
254
+ repo_id: str # username/space-name
255
+ code: str
256
+ language: str
257
+ pr_title: Optional[str] = None
258
+ pr_description: Optional[str] = None
259
+
260
+
261
+ class PullRequestResponse(BaseModel):
262
+ success: bool
263
+ message: str
264
+ pr_url: Optional[str] = None
265
+
266
+
267
+ class DuplicateSpaceRequest(BaseModel):
268
+ from_space_id: str # username/space-name
269
+ to_space_name: Optional[str] = None # Just the name, not full ID
270
+ private: bool = False
271
+
272
+
273
+ class DuplicateSpaceResponse(BaseModel):
274
+ success: bool
275
+ message: str
276
+ space_url: Optional[str] = None
277
+ space_id: Optional[str] = None
278
+
279
+
280
+ # Mock authentication for development
281
+ # In production, integrate with HuggingFace OAuth
282
+ class MockAuth:
283
+ def __init__(self, token: Optional[str] = None, username: Optional[str] = None):
284
+ self.token = token
285
+ self.username = username
286
+
287
+ def is_authenticated(self):
288
+ return bool(self.token)
289
+
290
+
291
+ def get_auth_from_header(authorization: Optional[str] = None):
292
+ """Extract authentication from header or session token"""
293
+ if not authorization:
294
+ return MockAuth(None, None)
295
+
296
+ # Handle "Bearer " prefix
297
+ if authorization.startswith("Bearer "):
298
+ token = authorization.replace("Bearer ", "")
299
+ else:
300
+ token = authorization
301
+
302
+ # Check if this is a session token (UUID format)
303
+ if token and "-" in token and len(token) > 20:
304
+ # Look up the session to get user info
305
+ if token in user_sessions:
306
+ session = user_sessions[token]
307
+ username = session.get("username")
308
+
309
+ # If username is missing from session (e.g., old session), try to fetch it
310
+ if not username and session.get("user_info"):
311
+ user_info = session["user_info"]
312
+ # Use same order as OAuth callback for consistency
313
+ username = (
314
+ user_info.get("preferred_username") or
315
+ user_info.get("name") or
316
+ user_info.get("sub") or
317
+ user_info.get("username") or
318
+ "user"
319
+ )
320
+ # Update the session with the username for future requests
321
+ session["username"] = username
322
+ print(f"[Auth] Extracted and cached username from user_info: {username}")
323
+
324
+ return MockAuth(session["access_token"], username)
325
+
326
+ # Dev token format: dev_token_<username>_<timestamp>
327
+ if token and token.startswith("dev_token_"):
328
+ parts = token.split("_")
329
+ username = parts[2] if len(parts) > 2 else "user"
330
+ return MockAuth(token, username)
331
+
332
+ # Regular OAuth access token passed directly - try to fetch username from HF
333
+ # This happens when frontend sends OAuth token after OAuth callback
334
+ if token and len(token) > 20:
335
+ try:
336
+ from huggingface_hub import HfApi
337
+ hf_api = HfApi(token=token)
338
+ user_info = hf_api.whoami()
339
+ username = (
340
+ user_info.get("preferred_username") or
341
+ user_info.get("name") or
342
+ user_info.get("sub") or
343
+ "user"
344
+ )
345
+ print(f"[Auth] Fetched username from OAuth token: {username}")
346
+ return MockAuth(token, username)
347
+ except Exception as e:
348
+ print(f"[Auth] Could not fetch username from OAuth token: {e}")
349
+ # Return with token but no username - deployment will try to fetch it
350
+ return MockAuth(token, None)
351
+
352
+ # Fallback: token with no username
353
+ return MockAuth(token, None)
354
+
355
+
356
+ @app.get("/")
357
+ async def root():
358
+ """Health check endpoint"""
359
+ return {"status": "ok", "message": "AnyCoder API is running"}
360
+
361
+
362
+ @app.get("/api/models", response_model=List[ModelInfo])
363
+ async def get_models():
364
+ """Get available AI models"""
365
+ return [
366
+ ModelInfo(
367
+ name=model["name"],
368
+ id=model["id"],
369
+ description=model["description"]
370
+ )
371
+ for model in AVAILABLE_MODELS
372
+ ]
373
+
374
+
375
+ @app.get("/api/languages")
376
+ async def get_languages():
377
+ """Get available programming languages/frameworks"""
378
+ return {"languages": LANGUAGE_CHOICES}
379
+
380
+
381
+ @app.get("/api/auth/login")
382
+ async def oauth_login(request: Request):
383
+ """Initiate OAuth login flow"""
384
+ # Generate a random state to prevent CSRF
385
+ state = secrets.token_urlsafe(32)
386
+ oauth_states[state] = {"timestamp": datetime.now()}
387
+
388
+ # Build redirect URI
389
+ protocol = "https" if SPACE_HOST and not SPACE_HOST.startswith("localhost") else "http"
390
+ redirect_uri = f"{protocol}://{SPACE_HOST}/api/auth/callback"
391
+
392
+ # Build authorization URL
393
+ auth_url = (
394
+ f"{OPENID_PROVIDER_URL}/oauth/authorize"
395
+ f"?client_id={OAUTH_CLIENT_ID}"
396
+ f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
397
+ f"&scope={urllib.parse.quote(OAUTH_SCOPES)}"
398
+ f"&state={state}"
399
+ f"&response_type=code"
400
+ )
401
+
402
+ return JSONResponse({"login_url": auth_url, "state": state})
403
+
404
+
405
+ @app.get("/api/auth/callback")
406
+ async def oauth_callback(code: str, state: str, request: Request):
407
+ """Handle OAuth callback"""
408
+ # Verify state to prevent CSRF
409
+ if state not in oauth_states:
410
+ raise HTTPException(status_code=400, detail="Invalid state parameter")
411
+
412
+ # Clean up old states
413
+ oauth_states.pop(state, None)
414
+
415
+ # Exchange code for tokens
416
+ protocol = "https" if SPACE_HOST and not SPACE_HOST.startswith("localhost") else "http"
417
+ redirect_uri = f"{protocol}://{SPACE_HOST}/api/auth/callback"
418
+
419
+ # Prepare authorization header
420
+ auth_string = f"{OAUTH_CLIENT_ID}:{OAUTH_CLIENT_SECRET}"
421
+ auth_bytes = auth_string.encode('utf-8')
422
+ auth_b64 = base64.b64encode(auth_bytes).decode('utf-8')
423
+
424
+ async with httpx.AsyncClient() as client:
425
+ try:
426
+ token_response = await client.post(
427
+ f"{OPENID_PROVIDER_URL}/oauth/token",
428
+ data={
429
+ "client_id": OAUTH_CLIENT_ID,
430
+ "code": code,
431
+ "grant_type": "authorization_code",
432
+ "redirect_uri": redirect_uri,
433
+ },
434
+ headers={
435
+ "Authorization": f"Basic {auth_b64}",
436
+ "Content-Type": "application/x-www-form-urlencoded",
437
+ },
438
+ )
439
+ token_response.raise_for_status()
440
+ token_data = token_response.json()
441
+
442
+ # Get user info
443
+ access_token = token_data.get("access_token")
444
+ userinfo_response = await client.get(
445
+ f"{OPENID_PROVIDER_URL}/oauth/userinfo",
446
+ headers={"Authorization": f"Bearer {access_token}"},
447
+ )
448
+ userinfo_response.raise_for_status()
449
+ user_info = userinfo_response.json()
450
+
451
+ # Extract username - try multiple possible fields
452
+ username = (
453
+ user_info.get("preferred_username") or # Primary HF field
454
+ user_info.get("name") or # Alternative field
455
+ user_info.get("sub") or # OpenID subject
456
+ user_info.get("username") or # Generic username
457
+ "user" # Fallback
458
+ )
459
+
460
+ print(f"[OAuth] User info received: {user_info}")
461
+ print(f"[OAuth] Extracted username: {username}")
462
+
463
+ # Calculate token expiration
464
+ # OAuth tokens typically have expires_in in seconds
465
+ expires_in = token_data.get("expires_in", 28800) # Default 8 hours
466
+ expires_at = datetime.now() + timedelta(seconds=expires_in)
467
+
468
+ # Create session
469
+ session_token = secrets.token_urlsafe(32)
470
+ user_sessions[session_token] = {
471
+ "access_token": access_token,
472
+ "user_info": user_info,
473
+ "timestamp": datetime.now(),
474
+ "expires_at": expires_at,
475
+ "username": username,
476
+ "deployed_spaces": [] # Track deployed spaces for follow-up updates
477
+ }
478
+
479
+ print(f"[OAuth] Session created: {session_token[:10]}... for user: {username}")
480
+
481
+ # Redirect to frontend with session token
482
+ frontend_url = f"{protocol}://{SPACE_HOST}/?session={session_token}"
483
+ return RedirectResponse(url=frontend_url)
484
+
485
+ except httpx.HTTPError as e:
486
+ print(f"OAuth error: {e}")
487
+ raise HTTPException(status_code=500, detail=f"OAuth failed: {str(e)}")
488
+
489
+
490
+ async def validate_token_with_hf(access_token: str) -> bool:
491
+ """Validate token with HuggingFace API"""
492
+ try:
493
+ async with httpx.AsyncClient() as client:
494
+ response = await client.get(
495
+ f"{OPENID_PROVIDER_URL}/oauth/userinfo",
496
+ headers={"Authorization": f"Bearer {access_token}"},
497
+ timeout=5.0
498
+ )
499
+ return response.status_code == 200
500
+ except Exception as e:
501
+ print(f"[Auth] Token validation error: {e}")
502
+ return False
503
+
504
+
505
+ @app.get("/api/auth/session")
506
+ async def get_session(session: str):
507
+ """Get user info from session token"""
508
+ if session not in user_sessions:
509
+ raise HTTPException(status_code=401, detail="Invalid session")
510
+
511
+ session_data = user_sessions[session]
512
+
513
+ # Check if session has expired
514
+ if is_session_expired(session_data):
515
+ # Clean up expired session
516
+ user_sessions.pop(session, None)
517
+ raise HTTPException(status_code=401, detail="Session expired. Please sign in again.")
518
+
519
+ # Validate token with HuggingFace
520
+ if not await validate_token_with_hf(session_data["access_token"]):
521
+ # Token is invalid, clean up session
522
+ user_sessions.pop(session, None)
523
+ raise HTTPException(status_code=401, detail="Authentication expired. Please sign in again.")
524
+
525
+ return {
526
+ "access_token": session_data["access_token"],
527
+ "user_info": session_data["user_info"],
528
+ }
529
+
530
+
531
+ @app.get("/api/auth/status")
532
+ async def auth_status(authorization: Optional[str] = Header(None)):
533
+ """Check authentication status and validate token"""
534
+ auth = get_auth_from_header(authorization)
535
+
536
+ if not auth.is_authenticated():
537
+ return AuthStatus(
538
+ authenticated=False,
539
+ username=None,
540
+ message="Not authenticated"
541
+ )
542
+
543
+ # For dev tokens, skip validation
544
+ if auth.token and auth.token.startswith("dev_token_"):
545
+ return AuthStatus(
546
+ authenticated=True,
547
+ username=auth.username,
548
+ message=f"Authenticated as {auth.username} (dev mode)"
549
+ )
550
+
551
+ # For session tokens, check expiration and validate
552
+ token = authorization.replace("Bearer ", "") if authorization else None
553
+ if token and "-" in token and len(token) > 20 and token in user_sessions:
554
+ session_data = user_sessions[token]
555
+
556
+ # Check if session has expired
557
+ if is_session_expired(session_data):
558
+ # Clean up expired session
559
+ user_sessions.pop(token, None)
560
+ return AuthStatus(
561
+ authenticated=False,
562
+ username=None,
563
+ message="Session expired"
564
+ )
565
+
566
+ # Validate token with HuggingFace
567
+ if not await validate_token_with_hf(session_data["access_token"]):
568
+ # Token is invalid, clean up session
569
+ user_sessions.pop(token, None)
570
+ return AuthStatus(
571
+ authenticated=False,
572
+ username=None,
573
+ message="Authentication expired"
574
+ )
575
+
576
+ return AuthStatus(
577
+ authenticated=True,
578
+ username=auth.username,
579
+ message=f"Authenticated as {auth.username}"
580
+ )
581
+
582
+ # For direct OAuth tokens, validate with HF
583
+ if auth.token:
584
+ is_valid = await validate_token_with_hf(auth.token)
585
+ if is_valid:
586
+ return AuthStatus(
587
+ authenticated=True,
588
+ username=auth.username,
589
+ message=f"Authenticated as {auth.username}"
590
+ )
591
+ else:
592
+ return AuthStatus(
593
+ authenticated=False,
594
+ username=None,
595
+ message="Token expired or invalid"
596
+ )
597
+
598
+ return AuthStatus(
599
+ authenticated=False,
600
+ username=None,
601
+ message="Not authenticated"
602
+ )
603
+
604
+
605
+ def cleanup_generated_code(code: str, language: str) -> str:
606
+ """Remove LLM explanatory text and extract only the actual code"""
607
+ try:
608
+ original_code = code
609
+
610
+ # Special handling for transformers.js - don't clean, pass through as-is
611
+ # The parser will handle extracting the files from === markers
612
+ if language == "transformers.js":
613
+ return code
614
+
615
+ # Special handling for ComfyUI JSON
616
+ if language == "comfyui":
617
+ # Try to parse as JSON first
618
+ try:
619
+ json.loads(code)
620
+ return code # If it parses, return as-is
621
+ except json.JSONDecodeError:
622
+ pass
623
+
624
+ # Find the last } in the code
625
+ last_brace = code.rfind('}')
626
+ if last_brace != -1:
627
+ # Extract everything up to and including the last }
628
+ potential_json = code[:last_brace + 1]
629
+
630
+ # Try to find where the JSON actually starts
631
+ json_start = 0
632
+ if '```json' in potential_json:
633
+ match = re.search(r'```json\s*\n', potential_json)
634
+ if match:
635
+ json_start = match.end()
636
+ elif '```' in potential_json:
637
+ match = re.search(r'```\s*\n', potential_json)
638
+ if match:
639
+ json_start = match.end()
640
+
641
+ # Extract the JSON
642
+ cleaned_json = potential_json[json_start:].strip()
643
+ cleaned_json = re.sub(r'```\s*$', '', cleaned_json).strip()
644
+
645
+ # Validate
646
+ try:
647
+ json.loads(cleaned_json)
648
+ return cleaned_json
649
+ except json.JSONDecodeError:
650
+ pass
651
+
652
+ # General cleanup for code languages
653
+ # Remove markdown code blocks and extract code
654
+ if '```' in code:
655
+ # Pattern to match code blocks with language specifiers
656
+ patterns = [
657
+ r'```(?:html|HTML)\s*\n([\s\S]+?)(?:\n```|$)',
658
+ r'```(?:python|py|Python)\s*\n([\s\S]+?)(?:\n```|$)',
659
+ r'```(?:javascript|js|jsx|JavaScript)\s*\n([\s\S]+?)(?:\n```|$)',
660
+ r'```(?:typescript|ts|tsx|TypeScript)\s*\n([\s\S]+?)(?:\n```|$)',
661
+ r'```\s*\n([\s\S]+?)(?:\n```|$)', # Generic code block
662
+ ]
663
+
664
+ for pattern in patterns:
665
+ match = re.search(pattern, code, re.IGNORECASE)
666
+ if match:
667
+ code = match.group(1).strip()
668
+ break
669
+
670
+ # Remove common LLM explanatory patterns
671
+ # Remove lines that start with explanatory text
672
+ lines = code.split('\n')
673
+ cleaned_lines = []
674
+ in_code = False
675
+
676
+ for line in lines:
677
+ stripped = line.strip()
678
+
679
+ # Skip common explanatory patterns at the start
680
+ if not in_code and (
681
+ stripped.lower().startswith('here') or
682
+ stripped.lower().startswith('this') or
683
+ stripped.lower().startswith('the above') or
684
+ stripped.lower().startswith('note:') or
685
+ stripped.lower().startswith('explanation:') or
686
+ stripped.lower().startswith('to use') or
687
+ stripped.lower().startswith('usage:') or
688
+ stripped.lower().startswith('instructions:') or
689
+ stripped.startswith('===') and '===' in stripped # Section markers
690
+ ):
691
+ continue
692
+
693
+ # Once we hit actual code, we're in
694
+ if stripped and not stripped.startswith('#') and not stripped.startswith('//'):
695
+ in_code = True
696
+
697
+ cleaned_lines.append(line)
698
+
699
+ code = '\n'.join(cleaned_lines).strip()
700
+
701
+ # Remove trailing explanatory text after the code ends
702
+ # For HTML: remove everything after final closing tag
703
+ if language == "html":
704
+ # Find last </html> or </body> or </div> at root level
705
+ last_html = code.rfind('</html>')
706
+ last_body = code.rfind('</body>')
707
+ last_tag = max(last_html, last_body)
708
+ if last_tag != -1:
709
+ # Check if there's significant text after
710
+ after_tag = code[last_tag + 7:].strip() # +7 for </html> length
711
+ if after_tag and len(after_tag) > 100: # Significant explanatory text
712
+ code = code[:last_tag + 7].strip()
713
+
714
+ # For Python: remove text after the last function/class definition or code block
715
+ elif language in ["gradio", "streamlit", "daggr"]:
716
+ # Find the last line that looks like actual code (not comments or blank)
717
+ lines = code.split('\n')
718
+ last_code_line = -1
719
+ for i in range(len(lines) - 1, -1, -1):
720
+ stripped = lines[i].strip()
721
+ if stripped and not stripped.startswith('#') and not stripped.startswith('"""') and not stripped.startswith("'''"):
722
+ # This looks like actual code
723
+ last_code_line = i
724
+ break
725
+
726
+ if last_code_line != -1 and last_code_line < len(lines) - 5:
727
+ # If there are more than 5 lines after last code, likely explanatory
728
+ code = '\n'.join(lines[:last_code_line + 1])
729
+
730
+ # Return cleaned code or original if cleaning made it too short
731
+ if len(code) > 50:
732
+ return code
733
+ else:
734
+ return original_code
735
+
736
+ except Exception as e:
737
+ print(f"[Code Cleanup] Error for {language}: {e}")
738
+ return code
739
+
740
+
741
+ def extract_reasoning(code: str, language: str) -> str:
742
+ """Extract LLM reasoning/explanatory text that's outside the main code block"""
743
+ try:
744
+ if not code:
745
+ return ""
746
+
747
+ # 1. Check for <think> tags (e.g. from DeepSeek-R1 or newer GLM-4)
748
+ think_match = re.search(r'<think>([\s\S]*?)</think>', code, re.IGNORECASE)
749
+ if think_match:
750
+ return think_match.group(1).strip()
751
+
752
+ # 2. Extract everything outside of markdown code blocks
753
+ blocks = list(re.finditer(r'```(?:[\w]*)\s*\n([\s\S]*?)(?:\n```|$)', code, re.IGNORECASE))
754
+
755
+ if not blocks:
756
+ return ""
757
+
758
+ text_parts = []
759
+ last_end = 0
760
+ for match in blocks:
761
+ pre_text = code[last_end:match.start()].strip()
762
+ if pre_text and len(pre_text) > 10:
763
+ text_parts.append(pre_text)
764
+ last_end = match.end()
765
+
766
+ post_text = code[last_end:].strip()
767
+ if post_text and len(post_text) > 10:
768
+ text_parts.append(post_text)
769
+
770
+ return "\n\n".join(text_parts).strip()
771
+ except Exception as e:
772
+ print(f"[Reasoning Extraction] Error: {e}")
773
+ return ""
774
+
775
+
776
+ @app.post("/api/generate")
777
+ async def generate_code(
778
+ request: CodeGenerationRequest,
779
+ authorization: Optional[str] = Header(None)
780
+ ):
781
+ """Generate code based on user query - returns streaming response"""
782
+
783
+
784
+ # Dev mode: No authentication required - just use server's HF_TOKEN
785
+ # In production, you would check real OAuth tokens here
786
+
787
+ # Extract parameters from request body
788
+ query = request.query
789
+ language = request.language
790
+ model_id = request.model_id
791
+ provider = request.provider
792
+
793
+ async def event_stream() -> AsyncGenerator[str, None]:
794
+ """Stream generated code chunks"""
795
+ # Use the model_id from outer scope
796
+ selected_model_id = model_id
797
+
798
+ try:
799
+ # Fast model lookup using cache
800
+ selected_model = MODEL_CACHE.get(selected_model_id)
801
+ if not selected_model:
802
+ # Fallback to first available model (shouldn't happen often)
803
+ selected_model = AVAILABLE_MODELS[0]
804
+ selected_model_id = selected_model["id"]
805
+
806
+ # Track generated code
807
+ generated_code = ""
808
+
809
+ # Fast system prompt lookup using cache
810
+ system_prompt = SYSTEM_PROMPT_CACHE.get(language)
811
+ if not system_prompt:
812
+ # Format generic prompt only if needed
813
+ system_prompt = GENERIC_SYSTEM_PROMPT.format(language=language)
814
+
815
+ # Detect if this is a followup request for React apps
816
+ # Check if there's existing code in the conversation history
817
+ is_followup = False
818
+ if language == "react" and request.history:
819
+ # Check if there's any previous assistant message with code (indicating a followup)
820
+ for msg in request.history:
821
+ if isinstance(msg, dict):
822
+ role = msg.get('role', '')
823
+ content = msg.get('content', '')
824
+ elif isinstance(msg, list) and len(msg) >= 2:
825
+ role = msg[0]
826
+ content = msg[1]
827
+ else:
828
+ continue
829
+
830
+ # If we find previous code from assistant, this is a followup
831
+ if role == 'assistant' and ('===' in content or 'Dockerfile' in content or 'package.json' in content):
832
+ is_followup = True
833
+ print(f"[Generate] Detected React followup request")
834
+ break
835
+
836
+ # Use followup prompt for React if detected
837
+ if is_followup and language == "react":
838
+ system_prompt = REACT_FOLLOW_UP_SYSTEM_PROMPT
839
+ print(f"[Generate] Using React followup system prompt for targeted fixes")
840
+
841
+ # Get cached client (reuses connections)
842
+ client = get_cached_client(selected_model_id, provider)
843
+
844
+ # Get the real model ID with provider suffixes
845
+ actual_model_id = get_real_model_id(selected_model_id)
846
+
847
+ # Prepare messages (optimized - no string concatenation in hot path)
848
+ # Check if this is a vision model and we have an image
849
+ if request.image_url and selected_model_id == "zai-org/GLM-4.6V:zai-org":
850
+ # Vision model with image - use multi-modal format
851
+ user_content = [
852
+ {
853
+ "type": "text",
854
+ "text": f"Generate a {language} application: {query}"
855
+ },
856
+ {
857
+ "type": "image_url",
858
+ "image_url": {
859
+ "url": request.image_url
860
+ }
861
+ }
862
+ ]
863
+ messages = [
864
+ {"role": "system", "content": system_prompt},
865
+ {"role": "user", "content": user_content}
866
+ ]
867
+ else:
868
+ # Regular text-only model
869
+ user_content = f"Generate a {language} application: {query}"
870
+ messages = [
871
+ {"role": "system", "content": system_prompt},
872
+ {"role": "user", "content": user_content}
873
+ ]
874
+
875
+ # Stream the response
876
+ try:
877
+ # All models now use OpenAI-compatible API via HF Router or Inference API
878
+ stream = client.chat.completions.create(
879
+ model=actual_model_id,
880
+ messages=messages,
881
+ temperature=0.7,
882
+ max_tokens=10000,
883
+ stream=True
884
+ )
885
+
886
+ chunk_count = 0
887
+
888
+ # Only process stream if it exists
889
+ if stream:
890
+ # Optimized chunk processing
891
+ for chunk in stream:
892
+ chunk_content = None
893
+
894
+ # OpenAI format: chunk.choices[0].delta.content
895
+ try:
896
+ if chunk.choices and chunk.choices[0].delta.content:
897
+ chunk_content = chunk.choices[0].delta.content
898
+ except (AttributeError, IndexError):
899
+ continue
900
+
901
+ if chunk_content:
902
+ generated_code += chunk_content
903
+ chunk_count += 1
904
+
905
+ # Send chunk immediately - optimized JSON serialization
906
+ # Only yield control every 5 chunks to reduce overhead
907
+ if chunk_count % 5 == 0:
908
+ await asyncio.sleep(0)
909
+
910
+ # Build event data efficiently
911
+ event_data = json.dumps({
912
+ "type": "chunk",
913
+ "content": chunk_content
914
+ })
915
+ yield f"data: {event_data}\n\n"
916
+
917
+ # Extract reasoning before cleaning up
918
+ reasoning = extract_reasoning(generated_code, language)
919
+
920
+ # Clean up generated code (remove LLM explanatory text and markdown)
921
+ generated_code = cleanup_generated_code(generated_code, language)
922
+
923
+ # Send completion event (include reasoning for GLM-4.7)
924
+ completion_dict = {
925
+ "type": "complete",
926
+ "code": generated_code
927
+ }
928
+ if selected_model_id == "zai-org/GLM-4.7" and reasoning:
929
+ completion_dict["reasoning"] = reasoning
930
+
931
+ completion_data = json.dumps(completion_dict)
932
+ yield f"data: {completion_data}\n\n"
933
+
934
+ # Auto-deploy after code generation (if authenticated and not skipped)
935
+ auth = get_auth_from_header(authorization)
936
+
937
+ if request.skip_auto_deploy:
938
+ print(f"[Auto-Deploy] Skipped - PR creation will be handled by frontend")
939
+
940
+ if auth.is_authenticated() and not (auth.token and auth.token.startswith("dev_token_")) and not request.skip_auto_deploy:
941
+ try:
942
+ # Send deploying status
943
+ deploying_data = json.dumps({
944
+ "type": "deploying",
945
+ "message": "πŸš€ Deploying your app to HuggingFace Spaces..."
946
+ })
947
+ yield f"data: {deploying_data}\n\n"
948
+
949
+ # Import deployment function
950
+ from backend_deploy import deploy_to_huggingface_space
951
+
952
+ # Convert history to the format expected by deploy function
953
+ # History comes from frontend as [[role, content], ...]
954
+ history_list = []
955
+ if request.history:
956
+ for msg in request.history:
957
+ if isinstance(msg, list) and len(msg) >= 2:
958
+ # Already in correct format [[role, content], ...]
959
+ history_list.append([msg[0], msg[1]])
960
+ elif isinstance(msg, dict):
961
+ # Convert dict format to list format
962
+ role = msg.get('role', '')
963
+ content = msg.get('content', '')
964
+ if role and content:
965
+ history_list.append([role, content])
966
+
967
+ print(f"[Auto-Deploy] Starting deployment...")
968
+ print(f"[Auto-Deploy] - Language: {language}")
969
+ print(f"[Auto-Deploy] - History items: {len(history_list)}")
970
+ print(f"[Auto-Deploy] - Username: {auth.username}")
971
+ print(f"[Auto-Deploy] - Code length: {len(generated_code)}")
972
+ print(f"[Auto-Deploy] - Existing repo ID from request: {request.existing_repo_id}")
973
+
974
+ # Deploy the code (update existing space if provided)
975
+ success, message, space_url = deploy_to_huggingface_space(
976
+ code=generated_code,
977
+ language=language,
978
+ token=auth.token,
979
+ username=auth.username,
980
+ existing_repo_id=request.existing_repo_id, # Use duplicated/imported space
981
+ history=history_list
982
+ )
983
+
984
+ print(f"[Auto-Deploy] Deployment result:")
985
+ print(f"[Auto-Deploy] - Success: {success}")
986
+ print(f"[Auto-Deploy] - Message: {message}")
987
+ print(f"[Auto-Deploy] - Space URL: {space_url}")
988
+
989
+ if success and space_url:
990
+ # Send deployment success
991
+ deploy_success_data = json.dumps({
992
+ "type": "deployed",
993
+ "message": message,
994
+ "space_url": space_url
995
+ })
996
+ yield f"data: {deploy_success_data}\n\n"
997
+ else:
998
+ # Send deployment error (non-blocking - code generation still succeeded)
999
+ deploy_error_data = json.dumps({
1000
+ "type": "deploy_error",
1001
+ "message": f"⚠️ Deployment failed: {message}"
1002
+ })
1003
+ yield f"data: {deploy_error_data}\n\n"
1004
+ except Exception as deploy_error:
1005
+ # Log deployment error but don't fail the generation
1006
+ import traceback
1007
+ print(f"[Auto-Deploy] ========== DEPLOYMENT EXCEPTION ==========")
1008
+ print(f"[Auto-Deploy] Exception type: {type(deploy_error).__name__}")
1009
+ print(f"[Auto-Deploy] Error message: {str(deploy_error)}")
1010
+ print(f"[Auto-Deploy] Full traceback:")
1011
+ traceback.print_exc()
1012
+ print(f"[Auto-Deploy] ==========================================")
1013
+
1014
+ deploy_error_data = json.dumps({
1015
+ "type": "deploy_error",
1016
+ "message": f"⚠️ Deployment error: {str(deploy_error)}"
1017
+ })
1018
+ yield f"data: {deploy_error_data}\n\n"
1019
+ else:
1020
+ print(f"[Auto-Deploy] Skipped - authenticated: {auth.is_authenticated()}, token_exists: {auth.token is not None}, is_dev: {auth.token.startswith('dev_token_') if auth.token else False}")
1021
+
1022
+ except Exception as e:
1023
+ # Handle rate limiting and other API errors
1024
+ error_message = str(e)
1025
+ is_rate_limit = False
1026
+ error_type = type(e).__name__
1027
+
1028
+ # Check for OpenAI SDK rate limit errors
1029
+ if error_type == "RateLimitError" or "rate_limit" in error_type.lower():
1030
+ is_rate_limit = True
1031
+ # Check if this is a rate limit error (429 status code)
1032
+ elif hasattr(e, 'status_code') and e.status_code == 429:
1033
+ is_rate_limit = True
1034
+ # Check error message for rate limit indicators
1035
+ elif "429" in error_message or "rate limit" in error_message.lower() or "too many requests" in error_message.lower():
1036
+ is_rate_limit = True
1037
+
1038
+ if is_rate_limit:
1039
+ # Try to extract retry-after header or message
1040
+ retry_after = None
1041
+ if hasattr(e, 'response') and e.response:
1042
+ retry_after = e.response.headers.get('Retry-After') or e.response.headers.get('retry-after')
1043
+ # Also check if the error object has retry_after
1044
+ elif hasattr(e, 'retry_after'):
1045
+ retry_after = str(e.retry_after)
1046
+
1047
+ if selected_model_id == "x-ai/grok-4.1-fast" or selected_model_id.startswith("openrouter/"):
1048
+ error_message = "⏱️ Rate limit exceeded for OpenRouter model"
1049
+ if retry_after:
1050
+ error_message += f". Please wait {retry_after} seconds before trying again."
1051
+ else:
1052
+ error_message += ". Free tier allows up to 20 requests per minute. Please wait a moment and try again."
1053
+ else:
1054
+ error_message = f"⏱️ Rate limit exceeded. Please wait before trying again."
1055
+ if retry_after:
1056
+ error_message += f" Retry after {retry_after} seconds."
1057
+
1058
+ # Check for other common API errors
1059
+ elif hasattr(e, 'status_code'):
1060
+ if e.status_code == 401:
1061
+ error_message = "❌ Authentication failed. Please check your API key."
1062
+ elif e.status_code == 403:
1063
+ error_message = "❌ Access forbidden. Please check your API key permissions."
1064
+ elif e.status_code == 500 or e.status_code == 502 or e.status_code == 503:
1065
+ error_message = "❌ Service temporarily unavailable. Please try again later."
1066
+
1067
+ error_data = json.dumps({
1068
+ "type": "error",
1069
+ "message": error_message
1070
+ })
1071
+ yield f"data: {error_data}\n\n"
1072
+
1073
+ except Exception as e:
1074
+ # Fallback error handling
1075
+ error_message = str(e)
1076
+ # Check if it's a rate limit error in the exception message
1077
+ if "429" in error_message or "rate limit" in error_message.lower() or "too many requests" in error_message.lower():
1078
+ if selected_model_id == "x-ai/grok-4.1-fast" or selected_model_id.startswith("openrouter/"):
1079
+ error_message = "⏱️ Rate limit exceeded for OpenRouter model. Free tier allows up to 20 requests per minute. Please wait a moment and try again."
1080
+ else:
1081
+ error_message = "⏱️ Rate limit exceeded. Please wait before trying again."
1082
+
1083
+ error_data = json.dumps({
1084
+ "type": "error",
1085
+ "message": f"Generation error: {error_message}"
1086
+ })
1087
+ yield f"data: {error_data}\n\n"
1088
+
1089
+ return StreamingResponse(
1090
+ event_stream(),
1091
+ media_type="text/event-stream",
1092
+ headers={
1093
+ "Cache-Control": "no-cache, no-transform",
1094
+ "Connection": "keep-alive",
1095
+ "X-Accel-Buffering": "no",
1096
+ "Content-Encoding": "none",
1097
+ "Transfer-Encoding": "chunked"
1098
+ }
1099
+ )
1100
+
1101
+
1102
+ @app.post("/api/deploy")
1103
+ async def deploy(
1104
+ request: DeploymentRequest,
1105
+ authorization: Optional[str] = Header(None)
1106
+ ):
1107
+ """Deploy generated code to HuggingFace Spaces"""
1108
+ print(f"[Deploy] ========== NEW DEPLOYMENT REQUEST ==========")
1109
+ print(f"[Deploy] Authorization header present: {authorization is not None}")
1110
+ if authorization:
1111
+ auth_preview = authorization[:20] + "..." if len(authorization) > 20 else authorization
1112
+ print(f"[Deploy] Authorization preview: {auth_preview}")
1113
+
1114
+ auth = get_auth_from_header(authorization)
1115
+
1116
+ print(f"[Deploy] Auth object - is_authenticated: {auth.is_authenticated()}, username: {auth.username}, has_token: {auth.token is not None}")
1117
+
1118
+ if not auth.is_authenticated():
1119
+ raise HTTPException(status_code=401, detail="Authentication required")
1120
+
1121
+ # Check if this is dev mode (no real token)
1122
+ if auth.token and auth.token.startswith("dev_token_"):
1123
+ # In dev mode, open HF Spaces creation page
1124
+ from backend_deploy import detect_sdk_from_code
1125
+ base_url = "https://huggingface.co/new-space"
1126
+
1127
+ sdk = detect_sdk_from_code(request.code, request.language)
1128
+
1129
+ params = urllib.parse.urlencode({
1130
+ "name": request.space_name or "my-anycoder-app",
1131
+ "sdk": sdk
1132
+ })
1133
+
1134
+ # Prepare file content based on language
1135
+ if request.language in ["html", "transformers.js", "comfyui"]:
1136
+ file_path = "index.html"
1137
+ else:
1138
+ file_path = "app.py"
1139
+
1140
+ files_params = urllib.parse.urlencode({
1141
+ "files[0][path]": file_path,
1142
+ "files[0][content]": request.code
1143
+ })
1144
+
1145
+ space_url = f"{base_url}?{params}&{files_params}"
1146
+
1147
+ return {
1148
+ "success": True,
1149
+ "space_url": space_url,
1150
+ "message": "Dev mode: Please create the space manually",
1151
+ "dev_mode": True
1152
+ }
1153
+
1154
+ # Production mode with real OAuth token
1155
+ try:
1156
+ from backend_deploy import deploy_to_huggingface_space
1157
+
1158
+ # Get user token - should be the access_token from OAuth session
1159
+ user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
1160
+
1161
+ if not user_token:
1162
+ raise HTTPException(status_code=401, detail="No HuggingFace token available. Please sign in first.")
1163
+
1164
+ print(f"[Deploy] Attempting deployment with token (first 10 chars): {user_token[:10]}...")
1165
+ print(f"[Deploy] Request parameters - language: {request.language}, space_name: {request.space_name}, existing_repo_id: {request.existing_repo_id}")
1166
+
1167
+ # If username is missing, fetch it from HuggingFace API
1168
+ username = auth.username
1169
+ if not username:
1170
+ print(f"[Deploy] Username not found in auth, fetching from HuggingFace API...")
1171
+ try:
1172
+ from huggingface_hub import HfApi
1173
+ hf_api = HfApi(token=user_token)
1174
+ user_info = hf_api.whoami()
1175
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
1176
+ print(f"[Deploy] Fetched username from HF API: {username}")
1177
+ except Exception as e:
1178
+ print(f"[Deploy] Warning: Could not fetch username from HF API: {e}")
1179
+ # Continue without username - the deploy function will try to fetch it again
1180
+
1181
+ # Check for existing deployed space in this session
1182
+ session_token = authorization.replace("Bearer ", "") if authorization else None
1183
+ existing_repo_id = request.existing_repo_id
1184
+
1185
+ # PRIORITY 1: Check history for deployed/imported spaces (like Gradio version does)
1186
+ # This is more reliable than session tracking since history persists in frontend
1187
+ if request.history and username:
1188
+ print(f"[Deploy] ========== CHECKING HISTORY ==========")
1189
+ print(f"[Deploy] History length: {len(request.history)} messages")
1190
+ print(f"[Deploy] Username: {username}")
1191
+
1192
+ # Log each message in history for debugging
1193
+ for i, msg in enumerate(request.history):
1194
+ role = msg.get('role', 'unknown')
1195
+ content = msg.get('content', '')
1196
+ content_preview = content[:100] if content else ''
1197
+ print(f"[Deploy] Message {i+1}: role={role}, content_preview='{content_preview}...'")
1198
+
1199
+ print(f"[Deploy] ==========================================")
1200
+
1201
+ for msg in request.history:
1202
+ role = msg.get('role', '')
1203
+ content = msg.get('content', '')
1204
+
1205
+ # Check for deployment confirmations
1206
+ if role == 'assistant' and ('βœ… Deployed!' in content or 'βœ… Updated!' in content):
1207
+ import re
1208
+ print(f"[Deploy] πŸ” Found deployment message in history!")
1209
+ print(f"[Deploy] Content: {content[:200]}")
1210
+ match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', content)
1211
+ if match:
1212
+ history_space_id = match.group(1)
1213
+ print(f"[Deploy] βœ… EXTRACTED space ID from history: {history_space_id}")
1214
+ if not existing_repo_id:
1215
+ existing_repo_id = history_space_id
1216
+ print(f"[Deploy] βœ… WILL UPDATE EXISTING SPACE: {existing_repo_id}")
1217
+ break
1218
+ else:
1219
+ print(f"[Deploy] ⚠️ Deployment message found but couldn't extract space ID")
1220
+
1221
+ # Check for imports
1222
+ elif role == 'user' and 'import' in content.lower():
1223
+ import re
1224
+ match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', content)
1225
+ if match:
1226
+ imported_space = match.group(1)
1227
+ # Only use if user owns it
1228
+ if imported_space.startswith(f"{username}/"):
1229
+ print(f"[Deploy] βœ… Found imported space in history (user owns it): {imported_space}")
1230
+ if not existing_repo_id:
1231
+ existing_repo_id = imported_space
1232
+ break
1233
+ else:
1234
+ if not request.history:
1235
+ print(f"[Deploy] ⚠️ No history provided in request")
1236
+ if not username:
1237
+ print(f"[Deploy] ⚠️ No username available")
1238
+
1239
+ # PRIORITY 2: Check session for previously deployed spaces (fallback)
1240
+ # This helps when history isn't passed from frontend
1241
+ if not existing_repo_id and session_token and session_token in user_sessions:
1242
+ session = user_sessions[session_token]
1243
+
1244
+ # Ensure deployed_spaces exists (for backward compatibility with old sessions)
1245
+ if "deployed_spaces" not in session:
1246
+ session["deployed_spaces"] = []
1247
+
1248
+ deployed_spaces = session.get("deployed_spaces", [])
1249
+
1250
+ print(f"[Deploy] Checking session for existing spaces. Found {len(deployed_spaces)} deployed spaces.")
1251
+ for i, space in enumerate(deployed_spaces):
1252
+ print(f"[Deploy] Space {i+1}: repo_id={space.get('repo_id')}, language={space.get('language')}, timestamp={space.get('timestamp')}")
1253
+
1254
+ # Find the most recent space for this language
1255
+ for space in reversed(deployed_spaces):
1256
+ if space.get("language") == request.language:
1257
+ session_space_id = space.get("repo_id")
1258
+ print(f"[Deploy] βœ… Found existing space in session for {request.language}: {session_space_id}")
1259
+ existing_repo_id = session_space_id
1260
+ break
1261
+
1262
+ if not existing_repo_id:
1263
+ print(f"[Deploy] ⚠️ No existing space found for language: {request.language}")
1264
+ elif not existing_repo_id:
1265
+ print(f"[Deploy] ⚠️ No session found and no history provided. session_token: {session_token[:10] if session_token else 'None'}")
1266
+
1267
+ # Use the standalone deployment function
1268
+ print(f"[Deploy] ========== CALLING deploy_to_huggingface_space ==========")
1269
+ print(f"[Deploy] existing_repo_id: {existing_repo_id}")
1270
+ print(f"[Deploy] space_name: {request.space_name}")
1271
+ print(f"[Deploy] language: {request.language}")
1272
+ print(f"[Deploy] username: {username}")
1273
+ print(f"[Deploy] ==========================================================")
1274
+
1275
+ success, message, space_url = deploy_to_huggingface_space(
1276
+ code=request.code,
1277
+ language=request.language,
1278
+ space_name=request.space_name,
1279
+ token=user_token,
1280
+ username=username,
1281
+ description=request.description if hasattr(request, 'description') else None,
1282
+ private=False,
1283
+ existing_repo_id=existing_repo_id,
1284
+ commit_message=request.commit_message
1285
+ )
1286
+
1287
+ if success:
1288
+ # Extract repo_id from space_url
1289
+ repo_id = space_url.split("/spaces/")[-1] if space_url else None
1290
+ print(f"[Deploy] βœ… Success! Repo ID: {repo_id}")
1291
+ print(f"[Deploy] Space URL: {space_url}")
1292
+ print(f"[Deploy] Message: {message}")
1293
+
1294
+ # Track deployed space in session for follow-up updates
1295
+ if session_token and session_token in user_sessions:
1296
+ if repo_id:
1297
+ session = user_sessions[session_token]
1298
+
1299
+ # Ensure deployed_spaces exists
1300
+ if "deployed_spaces" not in session:
1301
+ session["deployed_spaces"] = []
1302
+
1303
+ deployed_spaces = session.get("deployed_spaces", [])
1304
+
1305
+ print(f"[Deploy] πŸ“ Tracking space in session...")
1306
+ print(f"[Deploy] Current deployed_spaces count: {len(deployed_spaces)}")
1307
+
1308
+ # Update or add the space
1309
+ space_entry = {
1310
+ "repo_id": repo_id,
1311
+ "language": request.language,
1312
+ "timestamp": datetime.now()
1313
+ }
1314
+
1315
+ # Remove old entry for same repo_id if exists
1316
+ old_count = len(deployed_spaces)
1317
+ deployed_spaces = [s for s in deployed_spaces if s.get("repo_id") != repo_id]
1318
+ if old_count != len(deployed_spaces):
1319
+ print(f"[Deploy] Removed old entry for {repo_id}")
1320
+
1321
+ # Also remove old entries for same language (keep only most recent per language)
1322
+ # This ensures we always update the same space for a given language
1323
+ deployed_spaces = [s for s in deployed_spaces if s.get("language") != request.language]
1324
+
1325
+ deployed_spaces.append(space_entry)
1326
+
1327
+ session["deployed_spaces"] = deployed_spaces
1328
+ print(f"[Deploy] βœ… Tracked space in session: {repo_id}")
1329
+ print(f"[Deploy] New deployed_spaces count: {len(deployed_spaces)}")
1330
+ print(f"[Deploy] All deployed spaces: {[s.get('repo_id') for s in deployed_spaces]}")
1331
+ else:
1332
+ print(f"[Deploy] ⚠️ Could not extract repo_id from space_url: {space_url}")
1333
+ else:
1334
+ if not session_token:
1335
+ print(f"[Deploy] ⚠️ No session_token provided for tracking")
1336
+ elif session_token not in user_sessions:
1337
+ print(f"[Deploy] ⚠️ Session not found: {session_token[:10]}...")
1338
+ print(f"[Deploy] Available sessions: {[k[:10] for k in list(user_sessions.keys())[:5]]}")
1339
+
1340
+ return {
1341
+ "success": True,
1342
+ "space_url": space_url,
1343
+ "message": message,
1344
+ "repo_id": repo_id
1345
+ }
1346
+ else:
1347
+ # Provide user-friendly error message based on the error
1348
+ if "401" in message or "Unauthorized" in message:
1349
+ raise HTTPException(
1350
+ status_code=401,
1351
+ detail="Authentication failed. Please sign in again with HuggingFace."
1352
+ )
1353
+ elif "403" in message or "Forbidden" in message or "Permission" in message:
1354
+ raise HTTPException(
1355
+ status_code=403,
1356
+ detail="Permission denied. Your HuggingFace token may not have the required permissions (manage-repos scope)."
1357
+ )
1358
+ else:
1359
+ raise HTTPException(
1360
+ status_code=500,
1361
+ detail=message
1362
+ )
1363
+
1364
+ except HTTPException:
1365
+ # Re-raise HTTP exceptions as-is
1366
+ raise
1367
+ except Exception as e:
1368
+ # Log the full error for debugging
1369
+ import traceback
1370
+ error_details = traceback.format_exc()
1371
+ print(f"[Deploy] Deployment error: {error_details}")
1372
+
1373
+ raise HTTPException(
1374
+ status_code=500,
1375
+ detail=f"Deployment failed: {str(e)}"
1376
+ )
1377
+
1378
+
1379
+ @app.post("/api/create-pr", response_model=PullRequestResponse)
1380
+ async def create_pull_request(
1381
+ request: PullRequestRequest,
1382
+ authorization: Optional[str] = Header(None)
1383
+ ):
1384
+ """Create a Pull Request on an existing HuggingFace Space with redesigned code"""
1385
+ print(f"[PR] ========== NEW PULL REQUEST ==========")
1386
+ print(f"[PR] Repo ID: {request.repo_id}")
1387
+ print(f"[PR] Language: {request.language}")
1388
+ print(f"[PR] PR Title: {request.pr_title}")
1389
+
1390
+ auth = get_auth_from_header(authorization)
1391
+
1392
+ if not auth.is_authenticated():
1393
+ raise HTTPException(status_code=401, detail="Authentication required")
1394
+
1395
+ # Check if this is dev mode
1396
+ if auth.token and auth.token.startswith("dev_token_"):
1397
+ return PullRequestResponse(
1398
+ success=False,
1399
+ message="Dev mode: PR creation not available in dev mode. Please use production authentication.",
1400
+ pr_url=None
1401
+ )
1402
+
1403
+ # Production mode with real OAuth token
1404
+ try:
1405
+ from backend_deploy import create_pull_request_on_space
1406
+
1407
+ user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
1408
+
1409
+ if not user_token:
1410
+ raise HTTPException(status_code=401, detail="No HuggingFace token available. Please sign in first.")
1411
+
1412
+ print(f"[PR] Creating PR with token (first 10 chars): {user_token[:10]}...")
1413
+
1414
+ # Create the pull request
1415
+ success, message, pr_url = create_pull_request_on_space(
1416
+ repo_id=request.repo_id,
1417
+ code=request.code,
1418
+ language=request.language,
1419
+ token=user_token,
1420
+ pr_title=request.pr_title,
1421
+ pr_description=request.pr_description
1422
+ )
1423
+
1424
+ print(f"[PR] Result:")
1425
+ print(f"[PR] - Success: {success}")
1426
+ print(f"[PR] - Message: {message}")
1427
+ print(f"[PR] - PR URL: {pr_url}")
1428
+
1429
+ if success:
1430
+ return PullRequestResponse(
1431
+ success=True,
1432
+ message=message,
1433
+ pr_url=pr_url
1434
+ )
1435
+ else:
1436
+ # Provide user-friendly error messages
1437
+ if "401" in message or "Unauthorized" in message:
1438
+ raise HTTPException(
1439
+ status_code=401,
1440
+ detail="Authentication failed. Please sign in again with HuggingFace."
1441
+ )
1442
+ elif "403" in message or "Forbidden" in message or "Permission" in message:
1443
+ raise HTTPException(
1444
+ status_code=403,
1445
+ detail="Permission denied. You may not have write access to this space."
1446
+ )
1447
+ else:
1448
+ raise HTTPException(
1449
+ status_code=500,
1450
+ detail=message
1451
+ )
1452
+
1453
+ except HTTPException:
1454
+ raise
1455
+ except Exception as e:
1456
+ import traceback
1457
+ error_details = traceback.format_exc()
1458
+ print(f"[PR] Error: {error_details}")
1459
+
1460
+ raise HTTPException(
1461
+ status_code=500,
1462
+ detail=f"Failed to create pull request: {str(e)}"
1463
+ )
1464
+
1465
+
1466
+ @app.post("/api/duplicate-space", response_model=DuplicateSpaceResponse)
1467
+ async def duplicate_space_endpoint(
1468
+ request: DuplicateSpaceRequest,
1469
+ authorization: Optional[str] = Header(None)
1470
+ ):
1471
+ """Duplicate a HuggingFace Space to the user's account"""
1472
+ print(f"[Duplicate] ========== DUPLICATE SPACE REQUEST ==========")
1473
+ print(f"[Duplicate] From: {request.from_space_id}")
1474
+ print(f"[Duplicate] To: {request.to_space_name or 'auto'}")
1475
+ print(f"[Duplicate] Private: {request.private}")
1476
+
1477
+ auth = get_auth_from_header(authorization)
1478
+
1479
+ if not auth.is_authenticated():
1480
+ raise HTTPException(status_code=401, detail="Authentication required")
1481
+
1482
+ # Check if this is dev mode
1483
+ if auth.token and auth.token.startswith("dev_token_"):
1484
+ return DuplicateSpaceResponse(
1485
+ success=False,
1486
+ message="Dev mode: Space duplication not available in dev mode. Please use production authentication.",
1487
+ space_url=None,
1488
+ space_id=None
1489
+ )
1490
+
1491
+ # Production mode with real OAuth token
1492
+ try:
1493
+ from backend_deploy import duplicate_space_to_user
1494
+
1495
+ user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
1496
+
1497
+ if not user_token:
1498
+ raise HTTPException(status_code=401, detail="No HuggingFace token available. Please sign in first.")
1499
+
1500
+ print(f"[Duplicate] Duplicating space with token (first 10 chars): {user_token[:10]}...")
1501
+
1502
+ # Duplicate the space
1503
+ success, message, space_url = duplicate_space_to_user(
1504
+ from_space_id=request.from_space_id,
1505
+ to_space_name=request.to_space_name,
1506
+ token=user_token,
1507
+ private=request.private
1508
+ )
1509
+
1510
+ print(f"[Duplicate] Result:")
1511
+ print(f"[Duplicate] - Success: {success}")
1512
+ print(f"[Duplicate] - Message: {message}")
1513
+ print(f"[Duplicate] - Space URL: {space_url}")
1514
+
1515
+ if success:
1516
+ # Extract space_id from URL
1517
+ space_id = space_url.split("/spaces/")[-1] if space_url else None
1518
+
1519
+ return DuplicateSpaceResponse(
1520
+ success=True,
1521
+ message=message,
1522
+ space_url=space_url,
1523
+ space_id=space_id
1524
+ )
1525
+ else:
1526
+ # Provide user-friendly error messages
1527
+ if "401" in message or "Unauthorized" in message:
1528
+ raise HTTPException(
1529
+ status_code=401,
1530
+ detail="Authentication failed. Please sign in again with HuggingFace."
1531
+ )
1532
+ elif "403" in message or "Forbidden" in message or "Permission" in message:
1533
+ raise HTTPException(
1534
+ status_code=403,
1535
+ detail="Permission denied. You may not have access to this space."
1536
+ )
1537
+ elif "404" in message or "not found" in message.lower():
1538
+ raise HTTPException(
1539
+ status_code=404,
1540
+ detail="Space not found. Please check the URL and try again."
1541
+ )
1542
+ else:
1543
+ raise HTTPException(
1544
+ status_code=500,
1545
+ detail=message
1546
+ )
1547
+
1548
+ except HTTPException:
1549
+ raise
1550
+ except Exception as e:
1551
+ import traceback
1552
+ error_details = traceback.format_exc()
1553
+ print(f"[Duplicate] Error: {error_details}")
1554
+
1555
+ raise HTTPException(
1556
+ status_code=500,
1557
+ detail=f"Failed to duplicate space: {str(e)}"
1558
+ )
1559
+
1560
+
1561
+ @app.post("/api/import", response_model=ImportResponse)
1562
+ async def import_project(request: ImportRequest):
1563
+ """
1564
+ Import a project from HuggingFace Space, HuggingFace Model, or GitHub repo
1565
+
1566
+ Supports URLs like:
1567
+ - https://huggingface.co/spaces/username/space-name
1568
+ - https://huggingface.co/username/model-name
1569
+ - https://github.com/username/repo-name
1570
+ """
1571
+ try:
1572
+ importer = ProjectImporter()
1573
+ result = importer.import_from_url(request.url)
1574
+
1575
+ # Handle model-specific prefer_local flag
1576
+ if request.prefer_local and result.get('metadata', {}).get('has_alternatives'):
1577
+ # Switch to local code if available
1578
+ local_code = result['metadata'].get('local_code')
1579
+ if local_code:
1580
+ result['code'] = local_code
1581
+ result['metadata']['code_type'] = 'local'
1582
+ result['message'] = result['message'].replace('inference', 'local')
1583
+
1584
+ # Check if user owns this repo (for HuggingFace Spaces)
1585
+ owned_by_user = False
1586
+ repo_id = None
1587
+
1588
+ if request.username and result['status'] == 'success':
1589
+ # Extract repo_id from URL
1590
+ url = result.get('url', '')
1591
+ if 'huggingface.co/spaces/' in url:
1592
+ # Extract username/repo from URL
1593
+ match = re.search(r'huggingface\.co/spaces/([^/]+/[^/?#]+)', url)
1594
+ if match:
1595
+ repo_id = match.group(1)
1596
+ # Check if user owns this space
1597
+ if repo_id.startswith(f"{request.username}/"):
1598
+ owned_by_user = True
1599
+ print(f"[Import] User {request.username} owns the imported space: {repo_id}")
1600
+
1601
+ # Add ownership info to response
1602
+ result['owned_by_user'] = owned_by_user
1603
+ result['repo_id'] = repo_id
1604
+
1605
+ return ImportResponse(**result)
1606
+
1607
+ except Exception as e:
1608
+ return ImportResponse(
1609
+ status="error",
1610
+ message=f"Import failed: {str(e)}",
1611
+ code="",
1612
+ language="unknown",
1613
+ url=request.url,
1614
+ metadata={},
1615
+ owned_by_user=False,
1616
+ repo_id=None
1617
+ )
1618
+
1619
+
1620
+ @app.get("/api/import/space/{username}/{space_name}")
1621
+ async def import_space(username: str, space_name: str):
1622
+ """Import a specific HuggingFace Space by username and space name"""
1623
+ try:
1624
+ importer = ProjectImporter()
1625
+ result = importer.import_space(username, space_name)
1626
+ return result
1627
+ except Exception as e:
1628
+ return {
1629
+ "status": "error",
1630
+ "message": f"Failed to import space: {str(e)}",
1631
+ "code": "",
1632
+ "language": "unknown",
1633
+ "url": f"https://huggingface.co/spaces/{username}/{space_name}",
1634
+ "metadata": {}
1635
+ }
1636
+
1637
+
1638
+ @app.get("/api/import/model/{path:path}")
1639
+ async def import_model(path: str, prefer_local: bool = False):
1640
+ """
1641
+ Import a specific HuggingFace Model by model ID
1642
+
1643
+ Example: /api/import/model/meta-llama/Llama-3.2-1B-Instruct
1644
+ """
1645
+ try:
1646
+ importer = ProjectImporter()
1647
+ result = importer.import_model(path, prefer_local=prefer_local)
1648
+ return result
1649
+ except Exception as e:
1650
+ return {
1651
+ "status": "error",
1652
+ "message": f"Failed to import model: {str(e)}",
1653
+ "code": "",
1654
+ "language": "python",
1655
+ "url": f"https://huggingface.co/{path}",
1656
+ "metadata": {}
1657
+ }
1658
+
1659
+
1660
+ @app.get("/api/import/github/{owner}/{repo}")
1661
+ async def import_github(owner: str, repo: str):
1662
+ """Import a GitHub repository by owner and repo name"""
1663
+ try:
1664
+ importer = ProjectImporter()
1665
+ result = importer.import_github_repo(owner, repo)
1666
+ return result
1667
+ except Exception as e:
1668
+ return {
1669
+ "status": "error",
1670
+ "message": f"Failed to import repository: {str(e)}",
1671
+ "code": "",
1672
+ "language": "python",
1673
+ "url": f"https://github.com/{owner}/{repo}",
1674
+ "metadata": {}
1675
+ }
1676
+
1677
+
1678
+ @app.websocket("/ws/generate")
1679
+ async def websocket_generate(websocket: WebSocket):
1680
+ """WebSocket endpoint for real-time code generation"""
1681
+ await websocket.accept()
1682
+
1683
+ try:
1684
+ while True:
1685
+ # Receive message from client
1686
+ data = await websocket.receive_json()
1687
+
1688
+ query = data.get("query")
1689
+ language = data.get("language", "html")
1690
+ model_id = data.get("model_id", "claude-opus-4.5")
1691
+
1692
+ # Send acknowledgment
1693
+ await websocket.send_json({
1694
+ "type": "status",
1695
+ "message": "Generating code..."
1696
+ })
1697
+
1698
+ # Mock code generation for now
1699
+ await asyncio.sleep(0.5)
1700
+
1701
+ # Send generated code in chunks
1702
+ sample_code = f"<!-- Generated {language} code -->\n<h1>Hello from AnyCoder!</h1>"
1703
+
1704
+ for i, char in enumerate(sample_code):
1705
+ await websocket.send_json({
1706
+ "type": "chunk",
1707
+ "content": char,
1708
+ "progress": (i + 1) / len(sample_code) * 100
1709
+ })
1710
+ await asyncio.sleep(0.01)
1711
+
1712
+ # Send completion
1713
+ await websocket.send_json({
1714
+ "type": "complete",
1715
+ "code": sample_code
1716
+ })
1717
+
1718
+ except WebSocketDisconnect:
1719
+ print("Client disconnected")
1720
+ except Exception as e:
1721
+ await websocket.send_json({
1722
+ "type": "error",
1723
+ "message": str(e)
1724
+ })
1725
+ await websocket.close()
1726
+
1727
+
1728
+ if __name__ == "__main__":
1729
+ import uvicorn
1730
+ uvicorn.run("backend_api:app", host="0.0.0.0", port=8000, reload=True)
1731
+
backend_deploy.py ADDED
@@ -0,0 +1,1703 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Standalone deployment utilities for publishing to HuggingFace Spaces.
3
+ No Gradio dependencies - can be used in backend API.
4
+ """
5
+ import os
6
+ import re
7
+ import json
8
+ import uuid
9
+ import tempfile
10
+ import shutil
11
+ import ast
12
+ from typing import Dict, List, Optional, Tuple
13
+ from pathlib import Path
14
+
15
+ from huggingface_hub import HfApi
16
+ from backend_models import get_inference_client, get_real_model_id
17
+ from backend_parsers import (
18
+ parse_transformers_js_output,
19
+ parse_html_code,
20
+ parse_python_requirements,
21
+ parse_multi_file_python_output,
22
+ parse_react_output,
23
+ strip_tool_call_markers,
24
+ remove_code_block,
25
+ extract_import_statements,
26
+ generate_requirements_txt_with_llm,
27
+ enforce_critical_versions
28
+ )
29
+
30
+
31
+ def prettify_comfyui_json_for_html(json_content: str) -> str:
32
+ """Convert ComfyUI JSON to stylized HTML display with download button"""
33
+ try:
34
+ # Parse and prettify the JSON
35
+ parsed_json = json.loads(json_content)
36
+ prettified_json = json.dumps(parsed_json, indent=2, ensure_ascii=False)
37
+
38
+ # Create Apple-style HTML wrapper
39
+ html_content = f"""<!DOCTYPE html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="UTF-8">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
44
+ <title>ComfyUI Workflow</title>
45
+ <style>
46
+ * {{
47
+ margin: 0;
48
+ padding: 0;
49
+ box-sizing: border-box;
50
+ }}
51
+ body {{
52
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
53
+ background-color: #000000;
54
+ color: #f5f5f7;
55
+ line-height: 1.6;
56
+ padding: 20px;
57
+ min-height: 100vh;
58
+ }}
59
+ .container {{
60
+ max-width: 1200px;
61
+ margin: 0 auto;
62
+ }}
63
+ .header {{
64
+ text-align: center;
65
+ margin-bottom: 40px;
66
+ padding: 40px 20px;
67
+ }}
68
+ .header h1 {{
69
+ font-size: 48px;
70
+ font-weight: 600;
71
+ color: #ffffff;
72
+ margin-bottom: 12px;
73
+ letter-spacing: -0.02em;
74
+ }}
75
+ .header p {{
76
+ font-size: 18px;
77
+ color: #86868b;
78
+ font-weight: 400;
79
+ }}
80
+ .controls {{
81
+ display: flex;
82
+ gap: 12px;
83
+ margin-bottom: 24px;
84
+ justify-content: center;
85
+ }}
86
+ .btn {{
87
+ padding: 12px 24px;
88
+ border: none;
89
+ border-radius: 24px;
90
+ font-size: 14px;
91
+ font-weight: 500;
92
+ cursor: pointer;
93
+ transition: all 0.2s;
94
+ font-family: inherit;
95
+ }}
96
+ .btn-primary {{
97
+ background: #ffffff;
98
+ color: #000000;
99
+ }}
100
+ .btn-primary:hover {{
101
+ background: #f5f5f7;
102
+ transform: scale(0.98);
103
+ }}
104
+ .btn-secondary {{
105
+ background: #1d1d1f;
106
+ color: #f5f5f7;
107
+ border: 1px solid #424245;
108
+ }}
109
+ .btn-secondary:hover {{
110
+ background: #2d2d2f;
111
+ transform: scale(0.98);
112
+ }}
113
+ .json-container {{
114
+ background-color: #1d1d1f;
115
+ border-radius: 16px;
116
+ padding: 32px;
117
+ overflow-x: auto;
118
+ border: 1px solid #424245;
119
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
120
+ }}
121
+ pre {{
122
+ margin: 0;
123
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
124
+ font-size: 13px;
125
+ line-height: 1.6;
126
+ white-space: pre-wrap;
127
+ word-wrap: break-word;
128
+ }}
129
+ .json-key {{
130
+ color: #9cdcfe;
131
+ }}
132
+ .json-string {{
133
+ color: #ce9178;
134
+ }}
135
+ .json-number {{
136
+ color: #b5cea8;
137
+ }}
138
+ .json-boolean {{
139
+ color: #569cd6;
140
+ }}
141
+ .json-null {{
142
+ color: #569cd6;
143
+ }}
144
+ .success {{
145
+ color: #30d158;
146
+ }}
147
+ @media (max-width: 768px) {{
148
+ .header h1 {{
149
+ font-size: 32px;
150
+ }}
151
+ .controls {{
152
+ flex-direction: column;
153
+ }}
154
+ .json-container {{
155
+ padding: 20px;
156
+ }}
157
+ }}
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <div class="container">
162
+ <div class="header">
163
+ <h1>ComfyUI Workflow</h1>
164
+ <p>View and download your workflow JSON</p>
165
+ </div>
166
+
167
+ <div class="controls">
168
+ <button class="btn btn-primary" onclick="downloadJSON()">Download JSON</button>
169
+ <button class="btn btn-secondary" onclick="copyToClipboard()">Copy to Clipboard</button>
170
+ </div>
171
+
172
+ <div class="json-container">
173
+ <pre id="json-content">{prettified_json}</pre>
174
+ </div>
175
+ </div>
176
+
177
+ <script>
178
+ function copyToClipboard() {{
179
+ const jsonContent = document.getElementById('json-content').textContent;
180
+ navigator.clipboard.writeText(jsonContent).then(() => {{
181
+ const btn = event.target;
182
+ const originalText = btn.textContent;
183
+ btn.textContent = 'Copied!';
184
+ btn.classList.add('success');
185
+ setTimeout(() => {{
186
+ btn.textContent = originalText;
187
+ btn.classList.remove('success');
188
+ }}, 2000);
189
+ }}).catch(err => {{
190
+ alert('Failed to copy to clipboard');
191
+ }});
192
+ }}
193
+
194
+ function downloadJSON() {{
195
+ const jsonContent = document.getElementById('json-content').textContent;
196
+ const blob = new Blob([jsonContent], {{ type: 'application/json' }});
197
+ const url = URL.createObjectURL(blob);
198
+ const a = document.createElement('a');
199
+ a.href = url;
200
+ a.download = 'comfyui_workflow.json';
201
+ document.body.appendChild(a);
202
+ a.click();
203
+ document.body.removeChild(a);
204
+ URL.revokeObjectURL(url);
205
+
206
+ const btn = event.target;
207
+ const originalText = btn.textContent;
208
+ btn.textContent = 'Downloaded!';
209
+ btn.classList.add('success');
210
+ setTimeout(() => {{
211
+ btn.textContent = originalText;
212
+ btn.classList.remove('success');
213
+ }}, 2000);
214
+ }}
215
+
216
+ // Add syntax highlighting
217
+ function highlightJSON() {{
218
+ const content = document.getElementById('json-content');
219
+ let html = content.innerHTML;
220
+
221
+ // Highlight different JSON elements
222
+ html = html.replace(/"([^"]+)":/g, '<span class="json-key">"$1":</span>');
223
+ html = html.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>');
224
+ html = html.replace(/: (-?\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>');
225
+ html = html.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>');
226
+ html = html.replace(/: null/g, ': <span class="json-null">null</span>');
227
+
228
+ content.innerHTML = html;
229
+ }}
230
+
231
+ // Apply syntax highlighting after page load
232
+ window.addEventListener('load', highlightJSON);
233
+ </script>
234
+ </body>
235
+ </html>"""
236
+ return html_content
237
+ except json.JSONDecodeError:
238
+ # If it's not valid JSON, return as-is wrapped in basic HTML
239
+ return f"""<!DOCTYPE html>
240
+ <html lang="en">
241
+ <head>
242
+ <meta charset="UTF-8">
243
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
244
+ <title>ComfyUI Workflow</title>
245
+ <style>
246
+ body {{
247
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
248
+ background-color: #000000;
249
+ color: #f5f5f7;
250
+ padding: 40px;
251
+ }}
252
+ pre {{
253
+ background: #1d1d1f;
254
+ padding: 24px;
255
+ border-radius: 12px;
256
+ overflow-x: auto;
257
+ }}
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <h1>ComfyUI Workflow</h1>
262
+ <p>Error: Invalid JSON format</p>
263
+ <pre>{json_content}</pre>
264
+ </body>
265
+ </html>"""
266
+ except Exception as e:
267
+ print(f"Error prettifying ComfyUI JSON: {e}")
268
+ return json_content
269
+
270
+
271
+ # Note: parse_transformers_js_output, parse_python_requirements, strip_tool_call_markers,
272
+ # remove_code_block, extract_import_statements, generate_requirements_txt_with_llm,
273
+ # and parse_multi_file_python_output are now imported from backend_parsers.py
274
+
275
+
276
+ def is_streamlit_code(code: str) -> bool:
277
+ """Check if code is Streamlit"""
278
+ return 'import streamlit' in code or 'streamlit.run' in code
279
+
280
+
281
+ def is_gradio_code(code: str) -> bool:
282
+ """Check if code is Gradio or Daggr"""
283
+ return 'import gradio' in code or 'gr.' in code or 'import daggr' in code or 'from daggr' in code
284
+
285
+
286
+ def detect_sdk_from_code(code: str, language: str) -> str:
287
+ """Detect the appropriate SDK from code and language"""
288
+ if language == "html":
289
+ return "static"
290
+ elif language == "transformers.js":
291
+ return "static"
292
+ elif language == "comfyui":
293
+ return "static"
294
+ elif language == "react":
295
+ return "docker"
296
+ elif language == "streamlit" or is_streamlit_code(code):
297
+ return "docker"
298
+ elif language == "gradio" or language == "daggr" or is_gradio_code(code):
299
+ return "gradio"
300
+ else:
301
+ return "gradio" # Default
302
+
303
+
304
+ def add_anycoder_tag_to_readme(api, repo_id: str, app_port: Optional[int] = None, sdk: Optional[str] = None) -> None:
305
+ """
306
+ Download existing README, add anycoder tag and app_port if needed, and upload back.
307
+ Preserves all existing README content and frontmatter.
308
+
309
+ Args:
310
+ api: HuggingFace API client
311
+ repo_id: Repository ID (username/space-name)
312
+ app_port: Optional port number to set for Docker spaces (e.g., 7860)
313
+ sdk: Optional SDK type (e.g., 'gradio', 'streamlit', 'docker', 'static')
314
+ """
315
+ try:
316
+ import tempfile
317
+ import re
318
+
319
+ # Download the existing README
320
+ readme_path = api.hf_hub_download(
321
+ repo_id=repo_id,
322
+ filename="README.md",
323
+ repo_type="space"
324
+ )
325
+
326
+ # Read the existing README content
327
+ with open(readme_path, 'r', encoding='utf-8') as f:
328
+ content = f.read()
329
+
330
+ # Parse frontmatter and content
331
+ if content.startswith('---'):
332
+ # Split frontmatter and body
333
+ parts = content.split('---', 2)
334
+ if len(parts) >= 3:
335
+ frontmatter = parts[1].strip()
336
+ body = parts[2] if len(parts) > 2 else ""
337
+
338
+ # Check if tags already exist
339
+ if 'tags:' in frontmatter:
340
+ # Add anycoder to existing tags if not present
341
+ if '- anycoder' not in frontmatter:
342
+ frontmatter = re.sub(r'(tags:\s*\n(?:\s*-\s*[^\n]+\n)*)', r'\1- anycoder\n', frontmatter)
343
+ else:
344
+ # Add tags section with anycoder
345
+ frontmatter += '\ntags:\n- anycoder'
346
+
347
+ # Add app_port if specified and not already present
348
+ if app_port is not None and 'app_port:' not in frontmatter:
349
+ frontmatter += f'\napp_port: {app_port}'
350
+
351
+ # For Gradio spaces, always set sdk_version to 6.0.2
352
+ if sdk == 'gradio':
353
+ if 'sdk_version:' in frontmatter:
354
+ # Update existing sdk_version
355
+ frontmatter = re.sub(r'sdk_version:\s*[^\n]+', 'sdk_version: 6.0.2', frontmatter)
356
+ print(f"[README] Updated sdk_version to 6.0.2 for Gradio space")
357
+ else:
358
+ # Add sdk_version
359
+ frontmatter += '\nsdk_version: 6.0.2'
360
+ print(f"[README] Added sdk_version: 6.0.2 for Gradio space")
361
+
362
+ # Reconstruct the README
363
+ new_content = f"---\n{frontmatter}\n---{body}"
364
+ else:
365
+ # Malformed frontmatter, just add tags at the end of frontmatter
366
+ new_content = content.replace('---', '---\ntags:\n- anycoder\n---', 1)
367
+ else:
368
+ # No frontmatter, add it at the beginning
369
+ app_port_line = f'\napp_port: {app_port}' if app_port else ''
370
+ sdk_version_line = '\nsdk_version: 6.0.2' if sdk == 'gradio' else ''
371
+ new_content = f"---\ntags:\n- anycoder{app_port_line}{sdk_version_line}\n---\n\n{content}"
372
+
373
+ # Upload the modified README
374
+ with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding='utf-8') as f:
375
+ f.write(new_content)
376
+ temp_path = f.name
377
+
378
+ api.upload_file(
379
+ path_or_fileobj=temp_path,
380
+ path_in_repo="README.md",
381
+ repo_id=repo_id,
382
+ repo_type="space"
383
+ )
384
+
385
+ os.unlink(temp_path)
386
+
387
+ except Exception as e:
388
+ print(f"Warning: Could not modify README.md to add anycoder tag: {e}")
389
+
390
+
391
+ def create_dockerfile_for_streamlit(space_name: str) -> str:
392
+ """Create Dockerfile for Streamlit app"""
393
+ return f"""FROM python:3.11-slim
394
+
395
+ WORKDIR /app
396
+
397
+ COPY requirements.txt .
398
+ RUN pip install --no-cache-dir -r requirements.txt
399
+
400
+ COPY . .
401
+
402
+ EXPOSE 7860
403
+
404
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
405
+ """
406
+
407
+
408
+ def create_dockerfile_for_react(space_name: str) -> str:
409
+ """Create Dockerfile for React app"""
410
+ return f"""FROM node:18-slim
411
+
412
+ # Use existing node user
413
+ USER node
414
+ ENV HOME=/home/node
415
+ ENV PATH=/home/node/.local/bin:$PATH
416
+
417
+ WORKDIR /home/node/app
418
+
419
+ COPY --chown=node:node package*.json ./
420
+ RUN npm install
421
+
422
+ COPY --chown=node:node . .
423
+ RUN npm run build
424
+
425
+ EXPOSE 7860
426
+
427
+ CMD ["npm", "start", "--", "-p", "7860"]
428
+ """
429
+
430
+
431
+ def extract_space_id_from_history(history: Optional[List], username: Optional[str] = None) -> Optional[str]:
432
+ """
433
+ Extract existing space ID from chat history (for updates after followups/imports)
434
+
435
+ Args:
436
+ history: Chat history (list of lists [[role, content], ...] or list of dicts)
437
+ username: Current username (to verify ownership of imported spaces)
438
+
439
+ Returns:
440
+ Space ID (username/space-name) if found, None otherwise
441
+ """
442
+ if not history:
443
+ return None
444
+
445
+ import re
446
+ existing_space = None
447
+
448
+ # Look through history for previous deployments or imports
449
+ for msg in history:
450
+ # Handle both list format [[role, content], ...] and dict format [{'role': ..., 'content': ...}, ...]
451
+ if isinstance(msg, list) and len(msg) >= 2:
452
+ role = msg[0]
453
+ content = msg[1]
454
+ elif isinstance(msg, dict):
455
+ role = msg.get('role', '')
456
+ content = msg.get('content', '')
457
+ else:
458
+ continue
459
+
460
+ # Check assistant messages for deployment confirmations
461
+ if role == 'assistant':
462
+ # Look for various deployment success patterns (case-insensitive)
463
+ content_lower = content.lower()
464
+ has_deployment_indicator = (
465
+ "deployed" in content_lower or
466
+ "updated" in content_lower or
467
+ "βœ…" in content # Check mark often indicates deployment success
468
+ )
469
+
470
+ if has_deployment_indicator:
471
+ # Look for space URL pattern
472
+ match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', content)
473
+ if match:
474
+ existing_space = match.group(1)
475
+ print(f"[Extract Space] Found existing space: {existing_space}")
476
+ break
477
+
478
+ # Check user messages for imports
479
+ elif role == 'user':
480
+ if "import" in content.lower() and "space" in content.lower():
481
+ # Extract space name from import message
482
+ match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', content)
483
+ if match:
484
+ imported_space = match.group(1)
485
+ # Only use imported space if user owns it (can update it)
486
+ if username and imported_space.startswith(f"{username}/"):
487
+ existing_space = imported_space
488
+ break
489
+ # If user doesn't own the imported space, we'll create a new one
490
+ # (existing_space remains None, triggering new deployment)
491
+
492
+ return existing_space
493
+
494
+
495
+ def deploy_to_huggingface_space(
496
+ code: str,
497
+ language: str,
498
+ space_name: Optional[str] = None,
499
+ token: Optional[str] = None,
500
+ username: Optional[str] = None,
501
+ description: Optional[str] = None,
502
+ private: bool = False,
503
+ existing_repo_id: Optional[str] = None,
504
+ commit_message: Optional[str] = None,
505
+ history: Optional[List[Dict]] = None
506
+ ) -> Tuple[bool, str, Optional[str]]:
507
+ """
508
+ Deploy code to HuggingFace Spaces (create new or update existing)
509
+
510
+ Args:
511
+ code: Generated code to deploy
512
+ language: Target language/framework (html, gradio, streamlit, react, transformers.js, comfyui)
513
+ space_name: Name for the space (auto-generated if None, ignored if existing_repo_id provided)
514
+ token: HuggingFace API token
515
+ username: HuggingFace username
516
+ description: Space description
517
+ private: Whether to make the space private (only for new spaces)
518
+ existing_repo_id: If provided (username/space-name), updates this space instead of creating new one
519
+ commit_message: Custom commit message (defaults to "Deploy from anycoder" or "Update from anycoder")
520
+ history: Chat history (list of dicts with 'role' and 'content') - used to detect followups/imports
521
+
522
+ Returns:
523
+ Tuple of (success: bool, message: str, space_url: Optional[str])
524
+ """
525
+ if not token:
526
+ token = os.getenv("HF_TOKEN")
527
+ if not token:
528
+ return False, "No HuggingFace token provided", None
529
+
530
+ try:
531
+ api = HfApi(token=token)
532
+
533
+ # Get username if not provided (needed for history tracking)
534
+ if not username:
535
+ try:
536
+ user_info = api.whoami()
537
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
538
+ except Exception as e:
539
+ pass # Will handle later if needed
540
+
541
+ # Check history for existing space if not explicitly provided
542
+ # This enables automatic updates for followup prompts and imported spaces
543
+ if not existing_repo_id and history:
544
+ existing_repo_id = extract_space_id_from_history(history, username)
545
+ if existing_repo_id:
546
+ print(f"[Deploy] Detected existing space from history: {existing_repo_id}")
547
+
548
+ # Determine if this is an update or new deployment
549
+ is_update = existing_repo_id is not None
550
+
551
+ print(f"[Deploy] ========== DEPLOYMENT DECISION ==========")
552
+ print(f"[Deploy] existing_repo_id provided: {existing_repo_id}")
553
+ print(f"[Deploy] history provided: {history is not None} (length: {len(history) if history else 0})")
554
+ print(f"[Deploy] username: {username}")
555
+ print(f"[Deploy] is_update: {is_update}")
556
+ print(f"[Deploy] language: {language}")
557
+ print(f"[Deploy] ============================================")
558
+
559
+ # For React space updates (followup changes), handle SEARCH/REPLACE blocks
560
+ if is_update and language == "react":
561
+ print(f"[Deploy] React space update - checking for search/replace blocks")
562
+
563
+ # Import search/replace utilities
564
+ from backend_search_replace import has_search_replace_blocks, parse_file_specific_changes, apply_search_replace_changes
565
+ from huggingface_hub import hf_hub_download
566
+
567
+ # Check if code contains search/replace blocks
568
+ if has_search_replace_blocks(code):
569
+ print(f"[Deploy] Detected SEARCH/REPLACE blocks - applying targeted changes")
570
+
571
+ # Parse file-specific changes from code
572
+ file_changes = parse_file_specific_changes(code)
573
+
574
+ # Download existing files from the space
575
+ try:
576
+ print(f"[Deploy] Downloading existing files from space: {existing_repo_id}")
577
+
578
+ # Get list of files in the space
579
+ space_files = api.list_repo_files(repo_id=existing_repo_id, repo_type="space")
580
+ print(f"[Deploy] Found {len(space_files)} files in space: {space_files}")
581
+
582
+ # Download relevant files (React/Next.js files)
583
+ react_file_patterns = ['.js', '.jsx', '.ts', '.tsx', '.css', '.json', 'Dockerfile']
584
+ existing_files = {}
585
+
586
+ for file_path in space_files:
587
+ # Skip non-code files
588
+ if any(file_path.endswith(ext) or ext in file_path for ext in react_file_patterns):
589
+ try:
590
+ downloaded_path = hf_hub_download(
591
+ repo_id=existing_repo_id,
592
+ filename=file_path,
593
+ repo_type="space",
594
+ token=token
595
+ )
596
+ with open(downloaded_path, 'r', encoding='utf-8') as f:
597
+ existing_files[file_path] = f.read()
598
+ print(f"[Deploy] Downloaded: {file_path} ({len(existing_files[file_path])} chars)")
599
+ except Exception as e:
600
+ print(f"[Deploy] Warning: Could not download {file_path}: {e}")
601
+
602
+ if not existing_files:
603
+ print(f"[Deploy] Warning: No React files found in space, falling back to full deployment")
604
+ else:
605
+ # Apply search/replace changes to the appropriate files
606
+ updated_files = []
607
+
608
+ # Check if changes are file-specific or global
609
+ if "__all__" in file_changes:
610
+ # Global changes - try to apply to all files
611
+ changes_text = file_changes["__all__"]
612
+ print(f"[Deploy] Applying global search/replace changes")
613
+
614
+ # Try to apply to each file
615
+ for file_path, original_content in existing_files.items():
616
+ modified_content = apply_search_replace_changes(original_content, changes_text)
617
+ if modified_content != original_content:
618
+ print(f"[Deploy] Modified {file_path}")
619
+ success, msg = update_space_file(
620
+ repo_id=existing_repo_id,
621
+ file_path=file_path,
622
+ content=modified_content,
623
+ token=token,
624
+ commit_message=commit_message or f"Update {file_path} from anycoder"
625
+ )
626
+ if success:
627
+ updated_files.append(file_path)
628
+ else:
629
+ print(f"[Deploy] Warning: Failed to update {file_path}: {msg}")
630
+ else:
631
+ # File-specific changes
632
+ for filename, changes_text in file_changes.items():
633
+ # Find the file in existing files (handle both with/without directory prefix)
634
+ matching_file = None
635
+ for file_path in existing_files.keys():
636
+ if file_path == filename or file_path.endswith('/' + filename):
637
+ matching_file = file_path
638
+ break
639
+
640
+ if matching_file:
641
+ original_content = existing_files[matching_file]
642
+ modified_content = apply_search_replace_changes(original_content, changes_text)
643
+
644
+ print(f"[Deploy] Applying changes to {matching_file}")
645
+ success, msg = update_space_file(
646
+ repo_id=existing_repo_id,
647
+ file_path=matching_file,
648
+ content=modified_content,
649
+ token=token,
650
+ commit_message=commit_message or f"Update {matching_file} from anycoder"
651
+ )
652
+
653
+ if success:
654
+ updated_files.append(matching_file)
655
+ else:
656
+ print(f"[Deploy] Warning: Failed to update {matching_file}: {msg}")
657
+ else:
658
+ print(f"[Deploy] Warning: File {filename} not found in space")
659
+
660
+ if updated_files:
661
+ space_url = f"https://huggingface.co/spaces/{existing_repo_id}"
662
+ files_list = ", ".join(updated_files)
663
+ return True, f"βœ… Updated {len(updated_files)} file(s): {files_list}! View at: {space_url}", space_url
664
+ else:
665
+ return False, "No files were updated", None
666
+
667
+ except Exception as e:
668
+ print(f"[Deploy] Error applying search/replace changes: {e}")
669
+ import traceback
670
+ traceback.print_exc()
671
+ # Fall through to normal deployment
672
+ else:
673
+ print(f"[Deploy] No SEARCH/REPLACE blocks detected, proceeding with full file update")
674
+ # Fall through to normal React deployment below
675
+
676
+ # For Gradio space updates (import/redesign), update .py files and upload all new files
677
+ if is_update and language in ["gradio", "daggr"]:
678
+ print(f"[Deploy] Gradio space update - updating .py files and uploading any new files")
679
+
680
+ # Parse the code to get all files
681
+ files = parse_multi_file_python_output(code)
682
+
683
+ # Fallback if no files parsed
684
+ if not files:
685
+ print(f"[Deploy] No file markers found, using entire code as app.py")
686
+ cleaned_code = remove_code_block(code)
687
+ files['app.py'] = cleaned_code
688
+
689
+ if not files:
690
+ return False, "Error: No files found in generated code", None
691
+
692
+ print(f"[Deploy] Generated {len(files)} file(s): {list(files.keys())}")
693
+
694
+ # For redesign operations, ONLY update app.py to preserve other helper files
695
+ # Detect redesign from commit message OR from history (user prompt contains "redesign")
696
+ is_redesign = False
697
+ if commit_message and "redesign" in commit_message.lower():
698
+ is_redesign = True
699
+ elif history:
700
+ # Check last user message for "redesign" keyword
701
+ for role, content in reversed(history):
702
+ if role == "user" and content and "redesign" in content.lower():
703
+ is_redesign = True
704
+ break
705
+
706
+ if is_redesign:
707
+ print(f"[Deploy] Redesign operation detected - filtering to ONLY app.py")
708
+ app_py_content = files.get('app.py')
709
+ if not app_py_content:
710
+ return False, "Error: No app.py found in redesign output", None
711
+ files = {'app.py': app_py_content}
712
+ print(f"[Deploy] Will only update app.py ({len(app_py_content)} chars)")
713
+
714
+ # Upload all generated files (the LLM is instructed to only output .py files,
715
+ # but if it creates new assets/data files, we should upload those too)
716
+ # This approach updates .py files and adds any new files without touching
717
+ # existing non-.py files that weren't generated
718
+ updated_files = []
719
+ for file_path, content in files.items():
720
+ print(f"[Deploy] Uploading {file_path} ({len(content)} chars)")
721
+ success, msg = update_space_file(
722
+ repo_id=existing_repo_id,
723
+ file_path=file_path,
724
+ content=content,
725
+ token=token,
726
+ commit_message=commit_message or f"Update {file_path} from anycoder"
727
+ )
728
+
729
+ if success:
730
+ updated_files.append(file_path)
731
+ else:
732
+ print(f"[Deploy] Warning: Failed to update {file_path}: {msg}")
733
+
734
+ if updated_files:
735
+ space_url = f"https://huggingface.co/spaces/{existing_repo_id}"
736
+ files_list = ", ".join(updated_files)
737
+ return True, f"βœ… Updated {len(updated_files)} file(s): {files_list}! View at: {space_url}", space_url
738
+ else:
739
+ return False, "Failed to update any files", None
740
+
741
+ if is_update:
742
+ # Use existing repo
743
+ repo_id = existing_repo_id
744
+ space_name = existing_repo_id.split('/')[-1]
745
+ if '/' in existing_repo_id:
746
+ username = existing_repo_id.split('/')[0]
747
+ elif not username:
748
+ # Get username if still not available
749
+ try:
750
+ user_info = api.whoami()
751
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
752
+ except Exception as e:
753
+ return False, f"Failed to get user info: {str(e)}", None
754
+ else:
755
+ # Get username if not provided
756
+ if not username:
757
+ try:
758
+ user_info = api.whoami()
759
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
760
+ except Exception as e:
761
+ return False, f"Failed to get user info: {str(e)}", None
762
+
763
+ # Generate space name if not provided or empty
764
+ if not space_name or space_name.strip() == "":
765
+ space_name = f"anycoder-{uuid.uuid4().hex[:8]}"
766
+ print(f"[Deploy] Auto-generated space name: {space_name}")
767
+
768
+ # Clean space name (no spaces, lowercase, alphanumeric + hyphens)
769
+ space_name = re.sub(r'[^a-z0-9-]', '-', space_name.lower())
770
+ space_name = re.sub(r'-+', '-', space_name).strip('-')
771
+
772
+ # Ensure space_name is not empty after cleaning
773
+ if not space_name:
774
+ space_name = f"anycoder-{uuid.uuid4().hex[:8]}"
775
+ print(f"[Deploy] Space name was empty after cleaning, regenerated: {space_name}")
776
+
777
+ repo_id = f"{username}/{space_name}"
778
+ print(f"[Deploy] Using repo_id: {repo_id}")
779
+
780
+ # Detect SDK
781
+ sdk = detect_sdk_from_code(code, language)
782
+
783
+ # Create temporary directory for files
784
+ with tempfile.TemporaryDirectory() as temp_dir:
785
+ temp_path = Path(temp_dir)
786
+
787
+ # Parse code based on language
788
+ app_port = None # Track if we need app_port for Docker spaces
789
+ use_individual_uploads = False # Flag for transformers.js
790
+
791
+ if language == "transformers.js":
792
+ try:
793
+ files = parse_transformers_js_output(code)
794
+ print(f"[Deploy] Parsed transformers.js files: {list(files.keys())}")
795
+
796
+ # Log file sizes for debugging
797
+ for fname, fcontent in files.items():
798
+ if fcontent:
799
+ print(f"[Deploy] {fname}: {len(fcontent)} characters")
800
+ else:
801
+ print(f"[Deploy] {fname}: EMPTY")
802
+
803
+ # Validate all three files are present in the dict
804
+ required_files = {'index.html', 'index.js', 'style.css'}
805
+ missing_from_dict = required_files - set(files.keys())
806
+
807
+ if missing_from_dict:
808
+ error_msg = f"Failed to parse required files: {', '.join(sorted(missing_from_dict))}. "
809
+ error_msg += f"Parsed files: {', '.join(files.keys()) if files else 'none'}. "
810
+ error_msg += "Transformers.js apps require all three files (index.html, index.js, style.css). Please regenerate using the correct format."
811
+ print(f"[Deploy] {error_msg}")
812
+ return False, error_msg, None
813
+
814
+ # Validate files have actual content (not empty or whitespace-only)
815
+ empty_files = [name for name in required_files if not files.get(name, '').strip()]
816
+ if empty_files:
817
+ error_msg = f"Empty file content detected: {', '.join(sorted(empty_files))}. "
818
+ error_msg += "All three files must contain actual code. Please regenerate with complete content."
819
+ print(f"[Deploy] {error_msg}")
820
+ return False, error_msg, None
821
+
822
+ # Write transformers.js files to temp directory
823
+ for filename, content in files.items():
824
+ file_path = temp_path / filename
825
+ print(f"[Deploy] Writing {filename} ({len(content)} chars) to {file_path}")
826
+ # Use text mode - Python handles encoding automatically
827
+ if filename == "requirements.txt":
828
+ content = enforce_critical_versions(content)
829
+ file_path.write_text(content, encoding='utf-8')
830
+ # Verify the write was successful
831
+ written_size = file_path.stat().st_size
832
+ print(f"[Deploy] Verified {filename}: {written_size} bytes on disk")
833
+
834
+ # For transformers.js, we'll upload files individually (not via upload_folder)
835
+ use_individual_uploads = True
836
+
837
+ except Exception as e:
838
+ print(f"[Deploy] Error parsing transformers.js: {e}")
839
+ import traceback
840
+ traceback.print_exc()
841
+ return False, f"Error parsing transformers.js output: {str(e)}", None
842
+
843
+ elif language == "html":
844
+ html_code = parse_html_code(code)
845
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
846
+
847
+ elif language == "comfyui":
848
+ # ComfyUI is JSON, wrap in stylized HTML viewer with download button
849
+ html_code = prettify_comfyui_json_for_html(code)
850
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
851
+
852
+ elif language in ["gradio", "streamlit", "daggr"]:
853
+ files = parse_multi_file_python_output(code)
854
+
855
+ # Fallback: if no files parsed (missing === markers), treat entire code as app.py
856
+ if not files:
857
+ print(f"[Deploy] No file markers found in {language} code, using entire code as app.py")
858
+ # Clean up code blocks if present
859
+ cleaned_code = remove_code_block(code)
860
+ # Determine app filename based on language
861
+ app_filename = "streamlit_app.py" if language == "streamlit" else "app.py"
862
+ files[app_filename] = cleaned_code
863
+
864
+ # Write Python files (create subdirectories if needed)
865
+ for filename, content in files.items():
866
+ file_path = temp_path / filename
867
+ file_path.parent.mkdir(parents=True, exist_ok=True)
868
+ if filename == "requirements.txt":
869
+ content = enforce_critical_versions(content)
870
+ file_path.write_text(content, encoding='utf-8')
871
+
872
+ # Ensure requirements.txt exists - generate from imports if missing
873
+ if "requirements.txt" not in files:
874
+ # Get the main app file (app.py for gradio, streamlit_app.py or app.py for streamlit)
875
+ main_app = files.get('streamlit_app.py') or files.get('app.py', '')
876
+ if main_app:
877
+ print(f"[Deploy] Generating requirements.txt from imports in {language} app")
878
+ import_statements = extract_import_statements(main_app)
879
+ requirements_content = generate_requirements_txt_with_llm(import_statements)
880
+ (temp_path / "requirements.txt").write_text(requirements_content, encoding='utf-8')
881
+ print(f"[Deploy] Generated requirements.txt with {len(requirements_content.splitlines())} lines")
882
+ else:
883
+ # Fallback to minimal requirements if no app file found
884
+ if language == "gradio":
885
+ (temp_path / "requirements.txt").write_text("gradio>=4.0.0\n", encoding='utf-8')
886
+ elif language == "streamlit":
887
+ (temp_path / "requirements.txt").write_text("streamlit>=1.30.0\n", encoding='utf-8')
888
+ elif language == "daggr":
889
+ (temp_path / "requirements.txt").write_text("daggr>=0.5.4\ngradio>=6.0.2\n", encoding='utf-8')
890
+
891
+ # Create Dockerfile if needed
892
+ if sdk == "docker":
893
+ if language == "streamlit":
894
+ dockerfile = create_dockerfile_for_streamlit(space_name)
895
+ (temp_path / "Dockerfile").write_text(dockerfile, encoding='utf-8')
896
+ app_port = 7860 # Set app_port for Docker spaces
897
+ use_individual_uploads = True # Streamlit uses individual file uploads
898
+
899
+ elif language == "react":
900
+ # Parse React output to get all files (uses === filename === markers)
901
+ files = parse_react_output(code)
902
+
903
+ if not files:
904
+ return False, "Error: Could not parse React output", None
905
+
906
+ # If Dockerfile is missing, use template
907
+ if 'Dockerfile' not in files:
908
+ dockerfile = create_dockerfile_for_react(space_name)
909
+ files['Dockerfile'] = dockerfile
910
+
911
+ # Write all React files (create subdirectories if needed)
912
+ for filename, content in files.items():
913
+ file_path = temp_path / filename
914
+ file_path.parent.mkdir(parents=True, exist_ok=True)
915
+ file_path.write_text(content, encoding='utf-8')
916
+
917
+ app_port = 7860 # Set app_port for Docker spaces
918
+ use_individual_uploads = True # React uses individual file uploads
919
+
920
+ else:
921
+ # Default: treat as Gradio app
922
+ files = parse_multi_file_python_output(code)
923
+
924
+ # Fallback: if no files parsed (missing === markers), treat entire code as app.py
925
+ if not files:
926
+ print(f"[Deploy] No file markers found in default (gradio) code, using entire code as app.py")
927
+ # Clean up code blocks if present
928
+ cleaned_code = remove_code_block(code)
929
+ files['app.py'] = cleaned_code
930
+
931
+ # Write files (create subdirectories if needed)
932
+ for filename, content in files.items():
933
+ file_path = temp_path / filename
934
+ file_path.parent.mkdir(parents=True, exist_ok=True)
935
+ if filename == "requirements.txt":
936
+ content = enforce_critical_versions(content)
937
+ file_path.write_text(content, encoding='utf-8')
938
+
939
+ # Generate requirements.txt from imports if missing
940
+ if "requirements.txt" not in files:
941
+ main_app = files.get('app.py', '')
942
+ if main_app:
943
+ print(f"[Deploy] Generating requirements.txt from imports in default app")
944
+ import_statements = extract_import_statements(main_app)
945
+ requirements_content = generate_requirements_txt_with_llm(import_statements)
946
+ (temp_path / "requirements.txt").write_text(requirements_content, encoding='utf-8')
947
+ print(f"[Deploy] Generated requirements.txt with {len(requirements_content.splitlines())} lines")
948
+ else:
949
+ # Fallback to minimal requirements if no app file found
950
+ if language == "daggr":
951
+ (temp_path / "requirements.txt").write_text("daggr>=0.5.4\ngradio>=6.0.2\n", encoding='utf-8')
952
+ else:
953
+ (temp_path / "requirements.txt").write_text("gradio>=4.0.0\n", encoding='utf-8')
954
+
955
+ # Don't create README - HuggingFace will auto-generate it
956
+ # We'll add the anycoder tag after deployment
957
+
958
+ # ONLY create repo for NEW deployments of non-Docker, non-transformers.js spaces
959
+ # Docker and transformers.js handle repo creation separately below
960
+ # This matches the Gradio version logic (line 1256 in ui.py)
961
+ if not is_update and sdk != "docker" and language not in ["transformers.js"]:
962
+ print(f"[Deploy] Creating NEW {sdk} space: {repo_id}")
963
+ try:
964
+ api.create_repo(
965
+ repo_id=repo_id,
966
+ repo_type="space",
967
+ space_sdk=sdk,
968
+ private=private,
969
+ exist_ok=True
970
+ )
971
+ except Exception as e:
972
+ return False, f"Failed to create space: {str(e)}", None
973
+ elif is_update:
974
+ print(f"[Deploy] UPDATING existing space: {repo_id} (skipping create_repo)")
975
+
976
+ # Handle transformers.js spaces (create repo via duplicate_space)
977
+ if language == "transformers.js":
978
+ if not is_update:
979
+ print(f"[Deploy] Creating NEW transformers.js space via template duplication")
980
+ print(f"[Deploy] space_name value: '{space_name}' (type: {type(space_name)})")
981
+
982
+ # Safety check for space_name
983
+ if not space_name:
984
+ return False, "Internal error: space_name is None after generation", None
985
+
986
+ try:
987
+ from huggingface_hub import duplicate_space
988
+
989
+ # duplicate_space expects just the space name (not full repo_id)
990
+ # Use strip() to clean the space name
991
+ clean_space_name = space_name.strip()
992
+ print(f"[Deploy] Attempting to duplicate template space to: {clean_space_name}")
993
+
994
+ duplicated_repo = duplicate_space(
995
+ from_id="static-templates/transformers.js",
996
+ to_id=clean_space_name,
997
+ token=token,
998
+ exist_ok=True
999
+ )
1000
+ print(f"[Deploy] Template duplication result: {duplicated_repo} (type: {type(duplicated_repo)})")
1001
+ except Exception as e:
1002
+ print(f"[Deploy] Exception during duplicate_space: {type(e).__name__}: {str(e)}")
1003
+
1004
+ # Check if space actually exists (success despite error)
1005
+ space_exists = False
1006
+ try:
1007
+ if api.space_info(repo_id):
1008
+ space_exists = True
1009
+ except:
1010
+ pass
1011
+
1012
+ # Handle RepoUrl object "errors"
1013
+ error_msg = str(e)
1014
+ if ("'url'" in error_msg or "RepoUrl" in error_msg) and space_exists:
1015
+ print(f"[Deploy] Space exists despite RepoUrl error, continuing with deployment")
1016
+ else:
1017
+ # Fallback to regular create_repo
1018
+ print(f"[Deploy] Template duplication failed, attempting fallback to create_repo: {e}")
1019
+ try:
1020
+ api.create_repo(
1021
+ repo_id=repo_id,
1022
+ repo_type="space",
1023
+ space_sdk="static",
1024
+ private=private,
1025
+ exist_ok=True
1026
+ )
1027
+ print(f"[Deploy] Fallback create_repo successful")
1028
+ except Exception as e2:
1029
+ return False, f"Failed to create transformers.js space (both duplication and fallback failed): {str(e2)}", None
1030
+ else:
1031
+ # For updates, verify we can access the existing space
1032
+ try:
1033
+ space_info = api.space_info(repo_id)
1034
+ if not space_info:
1035
+ return False, f"Could not access space {repo_id} for update", None
1036
+ except Exception as e:
1037
+ return False, f"Cannot update space {repo_id}: {str(e)}", None
1038
+
1039
+ # Handle Docker spaces (React/Streamlit) - create repo separately
1040
+ elif sdk == "docker" and language in ["streamlit", "react"]:
1041
+ if not is_update:
1042
+ print(f"[Deploy] Creating NEW Docker space for {language}: {repo_id}")
1043
+ try:
1044
+ from huggingface_hub import create_repo as hf_create_repo
1045
+ hf_create_repo(
1046
+ repo_id=repo_id,
1047
+ repo_type="space",
1048
+ space_sdk="docker",
1049
+ token=token,
1050
+ exist_ok=True
1051
+ )
1052
+ except Exception as e:
1053
+ return False, f"Failed to create Docker space: {str(e)}", None
1054
+
1055
+ # Upload files
1056
+ if not commit_message:
1057
+ commit_message = "Update from anycoder" if is_update else "Deploy from anycoder"
1058
+
1059
+ try:
1060
+ if language == "transformers.js":
1061
+ # Special handling for transformers.js - create NEW temp files for each upload
1062
+ # This matches the working pattern in ui.py
1063
+ import time
1064
+
1065
+ # Get the parsed files from earlier
1066
+ files_to_upload = [
1067
+ ("index.html", files.get('index.html')),
1068
+ ("index.js", files.get('index.js')),
1069
+ ("style.css", files.get('style.css'))
1070
+ ]
1071
+
1072
+ max_attempts = 3
1073
+ for file_name, file_content in files_to_upload:
1074
+ if not file_content:
1075
+ return False, f"Missing content for {file_name}", None
1076
+
1077
+ success = False
1078
+ last_error = None
1079
+
1080
+ for attempt in range(max_attempts):
1081
+ temp_file_path = None
1082
+ try:
1083
+ # Create a NEW temp file for this upload (matches Gradio version approach)
1084
+ print(f"[Deploy] Creating temp file for {file_name} with {len(file_content)} chars")
1085
+ # Use text mode "w" - lets Python handle encoding automatically (better emoji support)
1086
+ with tempfile.NamedTemporaryFile("w", suffix=f".{file_name.split('.')[-1]}", delete=False) as f:
1087
+ f.write(file_content)
1088
+ temp_file_path = f.name
1089
+ # File is now closed and flushed, safe to upload
1090
+
1091
+ # Upload the file without commit_message (HF handles this for spaces)
1092
+ api.upload_file(
1093
+ path_or_fileobj=temp_file_path,
1094
+ path_in_repo=file_name,
1095
+ repo_id=repo_id,
1096
+ repo_type="space"
1097
+ )
1098
+ success = True
1099
+ print(f"[Deploy] Successfully uploaded {file_name}")
1100
+ break
1101
+
1102
+ except Exception as e:
1103
+ last_error = e
1104
+ error_str = str(e)
1105
+ print(f"[Deploy] Upload error for {file_name}: {error_str}")
1106
+ if "403" in error_str or "Forbidden" in error_str:
1107
+ return False, f"Permission denied uploading {file_name}. Check your token has write access to {repo_id}.", None
1108
+
1109
+ if attempt < max_attempts - 1:
1110
+ time.sleep(2) # Wait before retry
1111
+ print(f"[Deploy] Retry {attempt + 1}/{max_attempts} for {file_name}")
1112
+ finally:
1113
+ # Clean up temp file
1114
+ if temp_file_path and os.path.exists(temp_file_path):
1115
+ os.unlink(temp_file_path)
1116
+
1117
+ if not success:
1118
+ return False, f"Failed to upload {file_name} after {max_attempts} attempts: {last_error}", None
1119
+
1120
+ elif use_individual_uploads:
1121
+ # For React, Streamlit: upload each file individually
1122
+ import time
1123
+
1124
+ # Get list of files to upload from temp directory
1125
+ files_to_upload = []
1126
+ for file_path in temp_path.rglob('*'):
1127
+ if file_path.is_file():
1128
+ # Get relative path from temp directory (use forward slashes for repo paths)
1129
+ rel_path = file_path.relative_to(temp_path)
1130
+ files_to_upload.append(str(rel_path).replace('\\', '/'))
1131
+
1132
+ if not files_to_upload:
1133
+ return False, "No files to upload", None
1134
+
1135
+ print(f"[Deploy] Uploading {len(files_to_upload)} files individually: {files_to_upload}")
1136
+
1137
+ max_attempts = 3
1138
+ for filename in files_to_upload:
1139
+ # Convert back to Path for filesystem operations
1140
+ file_path = temp_path / filename.replace('/', os.sep)
1141
+ if not file_path.exists():
1142
+ return False, f"Failed to upload: {filename} not found", None
1143
+
1144
+ # Upload with retry logic
1145
+ success = False
1146
+ last_error = None
1147
+
1148
+ for attempt in range(max_attempts):
1149
+ try:
1150
+ # Upload without commit_message - HF API handles this for spaces
1151
+ api.upload_file(
1152
+ path_or_fileobj=str(file_path),
1153
+ path_in_repo=filename,
1154
+ repo_id=repo_id,
1155
+ repo_type="space"
1156
+ )
1157
+ success = True
1158
+ print(f"[Deploy] Successfully uploaded {filename}")
1159
+ break
1160
+ except Exception as e:
1161
+ last_error = e
1162
+ error_str = str(e)
1163
+ print(f"[Deploy] Upload error for {filename}: {error_str}")
1164
+ if "403" in error_str or "Forbidden" in error_str:
1165
+ return False, f"Permission denied uploading {filename}. Check your token has write access to {repo_id}.", None
1166
+ if attempt < max_attempts - 1:
1167
+ time.sleep(2) # Wait before retry
1168
+ print(f"[Deploy] Retry {attempt + 1}/{max_attempts} for {filename}")
1169
+
1170
+ if not success:
1171
+ return False, f"Failed to upload {filename} after {max_attempts} attempts: {last_error}", None
1172
+ else:
1173
+ # For other languages, use upload_folder
1174
+ print(f"[Deploy] Uploading folder to {repo_id}")
1175
+ api.upload_folder(
1176
+ folder_path=str(temp_path),
1177
+ repo_id=repo_id,
1178
+ repo_type="space"
1179
+ )
1180
+ except Exception as e:
1181
+ return False, f"Failed to upload files: {str(e)}", None
1182
+
1183
+ # After successful upload, modify the auto-generated README to add anycoder tag
1184
+ # For new spaces: HF auto-generates README, wait and modify it
1185
+ # For updates: README should already exist, just add tag if missing
1186
+ try:
1187
+ import time
1188
+ if not is_update:
1189
+ time.sleep(2) # Give HF time to generate README for new spaces
1190
+ add_anycoder_tag_to_readme(api, repo_id, app_port, sdk)
1191
+ except Exception as e:
1192
+ # Don't fail deployment if README modification fails
1193
+ print(f"Warning: Could not add anycoder tag to README: {e}")
1194
+
1195
+ # For transformers.js updates, trigger a space restart to ensure changes take effect
1196
+ if is_update and language == "transformers.js":
1197
+ try:
1198
+ api.restart_space(repo_id=repo_id)
1199
+ print(f"[Deploy] Restarted space after update: {repo_id}")
1200
+ except Exception as restart_error:
1201
+ # Don't fail the deployment if restart fails, just log it
1202
+ print(f"Note: Could not restart space after update: {restart_error}")
1203
+
1204
+ space_url = f"https://huggingface.co/spaces/{repo_id}"
1205
+ action = "Updated" if is_update else "Deployed"
1206
+
1207
+ # Include the space URL in the message for history tracking
1208
+ # This allows future deployments to detect this as the existing space
1209
+ success_msg = f"βœ… {action}! View your space at: {space_url}"
1210
+
1211
+ return True, success_msg, space_url
1212
+
1213
+ except Exception as e:
1214
+ print(f"[Deploy] Top-level exception caught: {type(e).__name__}: {str(e)}")
1215
+ import traceback
1216
+ traceback.print_exc()
1217
+ return False, f"Deployment error: {str(e)}", None
1218
+
1219
+
1220
+ def update_space_file(
1221
+ repo_id: str,
1222
+ file_path: str,
1223
+ content: str,
1224
+ token: Optional[str] = None,
1225
+ commit_message: Optional[str] = None
1226
+ ) -> Tuple[bool, str]:
1227
+ """
1228
+ Update a single file in an existing HuggingFace Space
1229
+
1230
+ Args:
1231
+ repo_id: Full repo ID (username/space-name)
1232
+ file_path: Path of file to update (e.g., "app.py")
1233
+ content: New file content
1234
+ token: HuggingFace API token
1235
+ commit_message: Commit message (default: "Update {file_path}")
1236
+
1237
+ Returns:
1238
+ Tuple of (success: bool, message: str)
1239
+ """
1240
+ if not token:
1241
+ token = os.getenv("HF_TOKEN")
1242
+ if not token:
1243
+ return False, "No HuggingFace token provided"
1244
+
1245
+ try:
1246
+ api = HfApi(token=token)
1247
+
1248
+ if not commit_message:
1249
+ commit_message = f"Update {file_path}"
1250
+
1251
+ # Create temporary file
1252
+ with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{file_path.split(".")[-1]}', delete=False) as f:
1253
+ f.write(content)
1254
+ temp_path = f.name
1255
+
1256
+ try:
1257
+ api.upload_file(
1258
+ path_or_fileobj=temp_path,
1259
+ path_in_repo=file_path,
1260
+ repo_id=repo_id,
1261
+ repo_type="space",
1262
+ commit_message=commit_message
1263
+ )
1264
+ return True, f"βœ… Successfully updated {file_path}"
1265
+ finally:
1266
+ os.unlink(temp_path)
1267
+
1268
+ except Exception as e:
1269
+ return False, f"Failed to update file: {str(e)}"
1270
+
1271
+
1272
+ def delete_space(
1273
+ repo_id: str,
1274
+ token: Optional[str] = None
1275
+ ) -> Tuple[bool, str]:
1276
+ """
1277
+ Delete a HuggingFace Space
1278
+
1279
+ Args:
1280
+ repo_id: Full repo ID (username/space-name)
1281
+ token: HuggingFace API token
1282
+
1283
+ Returns:
1284
+ Tuple of (success: bool, message: str)
1285
+ """
1286
+ if not token:
1287
+ token = os.getenv("HF_TOKEN")
1288
+ if not token:
1289
+ return False, "No HuggingFace token provided"
1290
+
1291
+ try:
1292
+ api = HfApi(token=token)
1293
+ api.delete_repo(repo_id=repo_id, repo_type="space")
1294
+ return True, f"βœ… Successfully deleted {repo_id}"
1295
+ except Exception as e:
1296
+ return False, f"Failed to delete space: {str(e)}"
1297
+
1298
+
1299
+ def list_user_spaces(
1300
+ username: Optional[str] = None,
1301
+ token: Optional[str] = None
1302
+ ) -> Tuple[bool, str, Optional[List[Dict]]]:
1303
+ """
1304
+ List all spaces for a user
1305
+
1306
+ Args:
1307
+ username: HuggingFace username (gets from token if None)
1308
+ token: HuggingFace API token
1309
+
1310
+ Returns:
1311
+ Tuple of (success: bool, message: str, spaces: Optional[List[Dict]])
1312
+ """
1313
+ if not token:
1314
+ token = os.getenv("HF_TOKEN")
1315
+ if not token:
1316
+ return False, "No HuggingFace token provided", None
1317
+
1318
+ try:
1319
+ api = HfApi(token=token)
1320
+
1321
+ # Get username if not provided
1322
+ if not username:
1323
+ user_info = api.whoami()
1324
+ username = user_info.get("name") or user_info.get("preferred_username")
1325
+
1326
+ # List spaces
1327
+ spaces = api.list_spaces(author=username)
1328
+
1329
+ space_list = []
1330
+ for space in spaces:
1331
+ space_list.append({
1332
+ "id": space.id,
1333
+ "author": space.author,
1334
+ "name": getattr(space, 'name', space.id.split('/')[-1]),
1335
+ "sdk": getattr(space, 'sdk', 'unknown'),
1336
+ "private": getattr(space, 'private', False),
1337
+ "url": f"https://huggingface.co/spaces/{space.id}"
1338
+ })
1339
+
1340
+ return True, f"Found {len(space_list)} spaces", space_list
1341
+
1342
+ except Exception as e:
1343
+ return False, f"Failed to list spaces: {str(e)}", None
1344
+
1345
+
1346
+ def duplicate_space_to_user(
1347
+ from_space_id: str,
1348
+ to_space_name: Optional[str] = None,
1349
+ token: Optional[str] = None,
1350
+ private: bool = False
1351
+ ) -> Tuple[bool, str, Optional[str]]:
1352
+ """
1353
+ Duplicate a HuggingFace Space to the user's account
1354
+
1355
+ Args:
1356
+ from_space_id: Source space ID (username/space-name)
1357
+ to_space_name: Destination space name (just the name, not full ID)
1358
+ token: HuggingFace API token
1359
+ private: Whether the duplicated space should be private
1360
+
1361
+ Returns:
1362
+ Tuple of (success: bool, message: str, space_url: Optional[str])
1363
+ """
1364
+ if not token:
1365
+ token = os.getenv("HF_TOKEN")
1366
+ if not token:
1367
+ return False, "No HuggingFace token provided", None
1368
+
1369
+ try:
1370
+ from huggingface_hub import duplicate_space
1371
+
1372
+ # Get username from token
1373
+ api = HfApi(token=token)
1374
+ user_info = api.whoami()
1375
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
1376
+
1377
+ # Get original space info to detect hardware and SDK
1378
+ print(f"[Duplicate] Fetching info for {from_space_id}")
1379
+ original_hardware = None
1380
+ original_storage = None
1381
+ original_sdk = None
1382
+ try:
1383
+ original_space_info = api.space_info(from_space_id)
1384
+ # Get SDK type
1385
+ original_sdk = getattr(original_space_info, 'sdk', None)
1386
+ # Get runtime info
1387
+ runtime = getattr(original_space_info, 'runtime', None)
1388
+ if runtime:
1389
+ original_hardware = getattr(runtime, 'hardware', None)
1390
+ original_storage = getattr(runtime, 'storage', None)
1391
+ print(f"[Duplicate] Original space SDK: {original_sdk}, hardware: {original_hardware}, storage: {original_storage}")
1392
+ except Exception as e:
1393
+ print(f"[Duplicate] Could not fetch space info: {e}")
1394
+
1395
+ # If no destination name provided, use original name
1396
+ if not to_space_name:
1397
+ # Extract original space name
1398
+ original_name = from_space_id.split('/')[-1]
1399
+ to_space_name = original_name
1400
+
1401
+ # Clean space name
1402
+ to_space_name = re.sub(r'[^a-z0-9-]', '-', to_space_name.lower())
1403
+ to_space_name = re.sub(r'-+', '-', to_space_name).strip('-')
1404
+
1405
+ # Construct full destination ID
1406
+ to_space_id = f"{username}/{to_space_name}"
1407
+
1408
+ print(f"[Duplicate] Duplicating {from_space_id} to {to_space_id}")
1409
+
1410
+ # Prepare duplicate_space parameters
1411
+ duplicate_params = {
1412
+ "from_id": from_space_id,
1413
+ "to_id": to_space_name, # Just the name, not full ID
1414
+ "token": token,
1415
+ "exist_ok": True
1416
+ }
1417
+
1418
+ # Hardware is REQUIRED by HF API for all space types when duplicating
1419
+ # Use detected hardware or default to cpu-basic
1420
+ hardware_to_use = original_hardware if original_hardware else "cpu-basic"
1421
+ duplicate_params["hardware"] = hardware_to_use
1422
+ print(f"[Duplicate] Hardware: {hardware_to_use} (SDK: {original_sdk}, original: {original_hardware})")
1423
+
1424
+ # Storage is optional
1425
+ if original_storage and original_storage.get('requested'):
1426
+ duplicate_params["storage"] = original_storage.get('requested')
1427
+ print(f"[Duplicate] Storage: {original_storage.get('requested')}")
1428
+
1429
+ # Only set private if explicitly requested
1430
+ if private:
1431
+ duplicate_params["private"] = private
1432
+
1433
+ # Duplicate the space
1434
+ print(f"[Duplicate] Duplicating {from_space_id} to {username}/{to_space_name}")
1435
+ print(f"[Duplicate] Parameters: {list(duplicate_params.keys())}")
1436
+
1437
+ try:
1438
+ duplicated_repo = duplicate_space(**duplicate_params)
1439
+ except Exception as dup_error:
1440
+ # Check if it's a zero-gpu hardware error
1441
+ error_str = str(dup_error).lower()
1442
+ if 'zero' in error_str or 'hardware' in error_str:
1443
+ print(f"[Duplicate] Hardware error detected (likely zero-gpu issue): {dup_error}")
1444
+ print(f"[Duplicate] Retrying with cpu-basic hardware...")
1445
+
1446
+ # Retry with cpu-basic hardware
1447
+ duplicate_params["hardware"] = "cpu-basic"
1448
+ try:
1449
+ duplicated_repo = duplicate_space(**duplicate_params)
1450
+ print(f"[Duplicate] βœ… Successfully duplicated with cpu-basic hardware")
1451
+ except Exception as retry_error:
1452
+ print(f"[Duplicate] Retry with cpu-basic also failed: {retry_error}")
1453
+ raise retry_error
1454
+ else:
1455
+ # Not a hardware error, re-raise
1456
+ raise dup_error
1457
+
1458
+ # Extract space URL
1459
+ space_url = f"https://huggingface.co/spaces/{to_space_id}"
1460
+
1461
+ success_msg = f"βœ… Space duplicated! View at: {space_url}"
1462
+ print(f"[Duplicate] {success_msg}")
1463
+
1464
+ return True, success_msg, space_url
1465
+
1466
+ except Exception as e:
1467
+ print(f"[Duplicate] Error: {type(e).__name__}: {str(e)}")
1468
+ import traceback
1469
+ traceback.print_exc()
1470
+ return False, f"Failed to duplicate space: {str(e)}", None
1471
+
1472
+
1473
+ def create_pull_request_on_space(
1474
+ repo_id: str,
1475
+ code: str,
1476
+ language: str,
1477
+ token: Optional[str] = None,
1478
+ pr_title: Optional[str] = None,
1479
+ pr_description: Optional[str] = None
1480
+ ) -> Tuple[bool, str, Optional[str]]:
1481
+ """
1482
+ Create a Pull Request on an existing HuggingFace Space with redesigned code
1483
+
1484
+ Args:
1485
+ repo_id: Full repo ID (username/space-name)
1486
+ code: New code to propose
1487
+ language: Language/framework type
1488
+ token: HuggingFace API token
1489
+ pr_title: Title for the PR (default: "Redesign from AnyCoder")
1490
+ pr_description: Description for the PR
1491
+
1492
+ Returns:
1493
+ Tuple of (success: bool, message: str, pr_url: Optional[str])
1494
+ """
1495
+ if not token:
1496
+ token = os.getenv("HF_TOKEN")
1497
+ if not token:
1498
+ return False, "No HuggingFace token provided", None
1499
+
1500
+ try:
1501
+ api = HfApi(token=token)
1502
+
1503
+ # Check if we can access the space first
1504
+ try:
1505
+ space_info = api.space_info(repo_id=repo_id, token=token)
1506
+ print(f"[PR] Space info: private={space_info.private if hasattr(space_info, 'private') else 'unknown'}")
1507
+
1508
+ # Check if space is private
1509
+ if hasattr(space_info, 'private') and space_info.private:
1510
+ return False, "❌ Cannot create PR on private space. The space must be public to accept PRs from others.", None
1511
+ except Exception as info_error:
1512
+ print(f"[PR] Could not fetch space info: {info_error}")
1513
+ # Continue anyway - maybe we can still create the PR
1514
+
1515
+ # Default PR title and description
1516
+ if not pr_title:
1517
+ pr_title = "🎨 Redesign from AnyCoder"
1518
+
1519
+ if not pr_description:
1520
+ pr_description = """This Pull Request contains a redesigned version of the app with:
1521
+
1522
+ - ✨ Modern, mobile-friendly design
1523
+ - 🎯 Minimal, clean components
1524
+ - πŸ“± Responsive layout
1525
+ - πŸš€ Improved user experience
1526
+
1527
+ Generated by [AnyCoder](https://huggingface.co/spaces/akhaliq/anycoder)"""
1528
+
1529
+ # Create temporary directory for files
1530
+ with tempfile.TemporaryDirectory() as temp_dir:
1531
+ temp_path = Path(temp_dir)
1532
+
1533
+ # Parse code based on language
1534
+ if language == "transformers.js":
1535
+ try:
1536
+ files = parse_transformers_js_output(code)
1537
+ print(f"[PR] Parsed transformers.js files: {list(files.keys())}")
1538
+
1539
+ # Write transformers.js files
1540
+ for filename, content in files.items():
1541
+ file_path = temp_path / filename
1542
+ if filename == "requirements.txt":
1543
+ content = enforce_critical_versions(content)
1544
+ file_path.write_text(content, encoding='utf-8')
1545
+
1546
+ except Exception as e:
1547
+ print(f"[PR] Error parsing transformers.js: {e}")
1548
+ return False, f"Error parsing transformers.js output: {str(e)}", None
1549
+
1550
+ elif language == "html":
1551
+ html_code = parse_html_code(code)
1552
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
1553
+
1554
+ elif language == "comfyui":
1555
+ html_code = prettify_comfyui_json_for_html(code)
1556
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
1557
+
1558
+ elif language in ["gradio", "streamlit", "react"]:
1559
+ files = parse_multi_file_python_output(code)
1560
+
1561
+ # Fallback if no files parsed
1562
+ if not files:
1563
+ print(f"[PR] No file markers found, using entire code as main file")
1564
+ cleaned_code = remove_code_block(code)
1565
+ if language == "streamlit":
1566
+ files["streamlit_app.py"] = cleaned_code
1567
+ elif language == "react":
1568
+ files["app.tsx"] = cleaned_code
1569
+ else:
1570
+ files["app.py"] = cleaned_code
1571
+
1572
+ # For Gradio PRs, only include .py files (preserve existing requirements.txt, etc.)
1573
+ # For redesigns, ONLY include app.py to avoid modifying helper files
1574
+ if language == "gradio":
1575
+ print(f"[PR] Gradio app - filtering to only .py files")
1576
+ py_files = {fname: content for fname, content in files.items() if fname.endswith('.py')}
1577
+ if not py_files:
1578
+ print(f"[PR] Warning: No .py files found in parsed output")
1579
+ return False, "No Python files found in generated code for Gradio PR", None
1580
+
1581
+ # Check if this is a redesign (pr_title contains "Redesign")
1582
+ is_redesign = "redesign" in pr_title.lower() if pr_title else False
1583
+
1584
+ if is_redesign:
1585
+ print(f"[PR] Redesign PR detected - filtering to ONLY app.py")
1586
+ if 'app.py' not in py_files:
1587
+ print(f"[PR] Warning: No app.py found in redesign output")
1588
+ return False, "No app.py found in redesign output for Gradio PR", None
1589
+ files = {'app.py': py_files['app.py']}
1590
+ print(f"[PR] Will only update app.py ({len(py_files['app.py'])} chars)")
1591
+ else:
1592
+ files = py_files
1593
+ print(f"[PR] Will update {len(files)} Python file(s): {list(files.keys())}")
1594
+
1595
+ # Write files (create subdirectories if needed)
1596
+ for filename, content in files.items():
1597
+ file_path = temp_path / filename
1598
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1599
+ if filename == "requirements.txt":
1600
+ content = enforce_critical_versions(content)
1601
+ file_path.write_text(content, encoding='utf-8')
1602
+
1603
+ # Skip requirements.txt generation for Gradio PRs (preserve existing)
1604
+ # For Streamlit, generate requirements.txt if missing
1605
+ if language in ["streamlit", "daggr"] and "requirements.txt" not in files:
1606
+ main_app = files.get('streamlit_app.py') or files.get('app.py', '')
1607
+ if main_app:
1608
+ print(f"[PR] Generating requirements.txt from imports")
1609
+ import_statements = extract_import_statements(main_app)
1610
+ requirements_content = generate_requirements_txt_with_llm(import_statements)
1611
+ (temp_path / "requirements.txt").write_text(requirements_content, encoding='utf-8')
1612
+
1613
+ else:
1614
+ # Default: treat as code file
1615
+ files = parse_multi_file_python_output(code)
1616
+ if not files:
1617
+ cleaned_code = remove_code_block(code)
1618
+ files['app.py'] = cleaned_code
1619
+
1620
+ for filename, content in files.items():
1621
+ file_path = temp_path / filename
1622
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1623
+ file_path.write_text(content, encoding='utf-8')
1624
+
1625
+ # Create PR with files using create_commit (recommended approach)
1626
+ # This creates the PR and uploads files in one API call
1627
+ try:
1628
+ print(f"[PR] Creating pull request with files on {repo_id}")
1629
+
1630
+ # Prepare operations for all files
1631
+ from huggingface_hub import CommitOperationAdd
1632
+ operations = []
1633
+
1634
+ for file_path in temp_path.rglob('*'):
1635
+ if file_path.is_file():
1636
+ rel_path = file_path.relative_to(temp_path)
1637
+ operations.append(
1638
+ CommitOperationAdd(
1639
+ path_in_repo=str(rel_path),
1640
+ path_or_fileobj=str(file_path)
1641
+ )
1642
+ )
1643
+
1644
+ print(f"[PR] Prepared {len(operations)} file operations")
1645
+ print(f"[PR] Token being used (first 20 chars): {token[:20] if token else 'None'}...")
1646
+
1647
+ # Create commit with PR (pass token explicitly)
1648
+ commit_info = api.create_commit(
1649
+ repo_id=repo_id,
1650
+ repo_type="space",
1651
+ operations=operations,
1652
+ commit_message=pr_title,
1653
+ commit_description=pr_description,
1654
+ create_pr=True, # This creates a PR with the changes
1655
+ token=token, # Explicitly pass token
1656
+ )
1657
+
1658
+ # Extract PR URL
1659
+ pr_url = commit_info.pr_url if hasattr(commit_info, 'pr_url') else None
1660
+ pr_num = commit_info.pr_num if hasattr(commit_info, 'pr_num') else None
1661
+
1662
+ if not pr_url and pr_num:
1663
+ pr_url = f"https://huggingface.co/spaces/{repo_id}/discussions/{pr_num}"
1664
+ elif not pr_url:
1665
+ pr_url = f"https://huggingface.co/spaces/{repo_id}/discussions"
1666
+
1667
+ print(f"[PR] Created PR: {pr_url}")
1668
+ success_msg = f"βœ… Pull Request created! View at: {pr_url}"
1669
+
1670
+ return True, success_msg, pr_url
1671
+
1672
+ except Exception as e:
1673
+ error_msg = str(e)
1674
+ print(f"[PR] Error creating pull request: {error_msg}")
1675
+ import traceback
1676
+ traceback.print_exc()
1677
+
1678
+ # Provide helpful error message based on the error type
1679
+ if "403" in error_msg or "Forbidden" in error_msg or "Authorization" in error_msg:
1680
+ user_msg = (
1681
+ "❌ Cannot create Pull Request: Permission denied.\n\n"
1682
+ "**Possible reasons:**\n"
1683
+ "- The space owner hasn't enabled Pull Requests\n"
1684
+ "- You don't have write access to this space\n"
1685
+ "- Spaces have stricter PR permissions than models/datasets\n\n"
1686
+ "**What you can do:**\n"
1687
+ "βœ… Use the 'Redesign' button WITHOUT checking 'Create PR' - this will:\n"
1688
+ " 1. Duplicate the space to your account\n"
1689
+ " 2. Apply the redesign to your copy\n"
1690
+ " 3. You'll own the new space!\n\n"
1691
+ "Or contact the space owner to enable Pull Requests."
1692
+ )
1693
+ else:
1694
+ user_msg = f"Failed to create pull request: {error_msg}"
1695
+
1696
+ return False, user_msg, None
1697
+
1698
+ except Exception as e:
1699
+ print(f"[PR] Top-level exception: {type(e).__name__}: {str(e)}")
1700
+ import traceback
1701
+ traceback.print_exc()
1702
+ return False, f"Pull request error: {str(e)}", None
1703
+
backend_docs_manager.py ADDED
@@ -0,0 +1,708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Documentation management for backend system prompts.
3
+ Handles fetching, caching, and updating documentation from llms.txt files.
4
+ No dependencies on Gradio or other heavy libraries - pure Python only.
5
+ """
6
+ import os
7
+ import re
8
+ from datetime import datetime
9
+ from typing import Optional
10
+
11
+ try:
12
+ import requests
13
+ HAS_REQUESTS = True
14
+ except ImportError:
15
+ HAS_REQUESTS = False
16
+ print("Warning: requests library not available, using minimal fallback")
17
+
18
+ # Configuration
19
+ GRADIO_LLMS_TXT_URL = "https://www.gradio.app/llms.txt"
20
+ GRADIO_DOCS_CACHE_FILE = ".backend_gradio_docs_cache.txt"
21
+ GRADIO_DOCS_LAST_UPDATE_FILE = ".backend_gradio_docs_last_update.txt"
22
+
23
+ TRANSFORMERSJS_DOCS_URL = "https://huggingface.co/docs/transformers.js/llms.txt"
24
+ TRANSFORMERSJS_DOCS_CACHE_FILE = ".backend_transformersjs_docs_cache.txt"
25
+ TRANSFORMERSJS_DOCS_LAST_UPDATE_FILE = ".backend_transformersjs_docs_last_update.txt"
26
+
27
+ COMFYUI_LLMS_TXT_URL = "https://docs.comfy.org/llms.txt"
28
+ COMFYUI_DOCS_CACHE_FILE = ".backend_comfyui_docs_cache.txt"
29
+ COMFYUI_DOCS_LAST_UPDATE_FILE = ".backend_comfyui_docs_last_update.txt"
30
+
31
+ # Global variable to store the current Gradio documentation
32
+ _gradio_docs_content: Optional[str] = None
33
+ _gradio_docs_last_fetched: Optional[datetime] = None
34
+
35
+ # Global variable to store the current transformers.js documentation
36
+ _transformersjs_docs_content: Optional[str] = None
37
+ _transformersjs_docs_last_fetched: Optional[datetime] = None
38
+
39
+ # Global variable to store the current ComfyUI documentation
40
+ _comfyui_docs_content: Optional[str] = None
41
+ _comfyui_docs_last_fetched: Optional[datetime] = None
42
+
43
+ def fetch_gradio_docs() -> Optional[str]:
44
+ """Fetch the latest Gradio documentation from llms.txt"""
45
+ if not HAS_REQUESTS:
46
+ return None
47
+
48
+ try:
49
+ response = requests.get(GRADIO_LLMS_TXT_URL, timeout=10)
50
+ response.raise_for_status()
51
+ return response.text
52
+ except Exception as e:
53
+ print(f"Warning: Failed to fetch Gradio docs from {GRADIO_LLMS_TXT_URL}: {e}")
54
+ return None
55
+
56
+ def fetch_transformersjs_docs() -> Optional[str]:
57
+ """Fetch the latest transformers.js documentation from llms.txt"""
58
+ if not HAS_REQUESTS:
59
+ return None
60
+
61
+ try:
62
+ response = requests.get(TRANSFORMERSJS_DOCS_URL, timeout=10)
63
+ response.raise_for_status()
64
+ return response.text
65
+ except Exception as e:
66
+ print(f"Warning: Failed to fetch transformers.js docs from {TRANSFORMERSJS_DOCS_URL}: {e}")
67
+ return None
68
+
69
+ def fetch_comfyui_docs() -> Optional[str]:
70
+ """Fetch the latest ComfyUI documentation from llms.txt"""
71
+ if not HAS_REQUESTS:
72
+ return None
73
+
74
+ try:
75
+ response = requests.get(COMFYUI_LLMS_TXT_URL, timeout=10)
76
+ response.raise_for_status()
77
+ return response.text
78
+ except Exception as e:
79
+ print(f"Warning: Failed to fetch ComfyUI docs from {COMFYUI_LLMS_TXT_URL}: {e}")
80
+ return None
81
+
82
+ def filter_problematic_instructions(content: str) -> str:
83
+ """Filter out problematic instructions that cause LLM to stop generation prematurely"""
84
+ if not content:
85
+ return content
86
+
87
+ # List of problematic phrases that cause early termination when LLM encounters ``` in user code
88
+ problematic_patterns = [
89
+ r"Output ONLY the code inside a ``` code block, and do not include any explanations or extra text",
90
+ r"output only the code inside a ```.*?``` code block",
91
+ r"Always output only the.*?code.*?inside.*?```.*?```.*?block",
92
+ r"Return ONLY the code inside a.*?```.*?``` code block",
93
+ r"Do NOT add the language name at the top of the code output",
94
+ r"do not include any explanations or extra text",
95
+ r"Always output only the.*?code blocks.*?shown above, and do not include any explanations",
96
+ r"Output.*?ONLY.*?code.*?inside.*?```.*?```",
97
+ r"Return.*?ONLY.*?code.*?inside.*?```.*?```",
98
+ r"Generate.*?ONLY.*?code.*?inside.*?```.*?```",
99
+ r"Provide.*?ONLY.*?code.*?inside.*?```.*?```",
100
+ ]
101
+
102
+ # Remove problematic patterns
103
+ filtered_content = content
104
+ for pattern in problematic_patterns:
105
+ # Use case-insensitive matching
106
+ filtered_content = re.sub(pattern, "", filtered_content, flags=re.IGNORECASE | re.DOTALL)
107
+
108
+ # Clean up any double newlines or extra whitespace left by removals
109
+ filtered_content = re.sub(r'\n\s*\n\s*\n', '\n\n', filtered_content)
110
+ filtered_content = re.sub(r'^\s+', '', filtered_content, flags=re.MULTILINE)
111
+
112
+ return filtered_content
113
+
114
+ def load_cached_gradio_docs() -> Optional[str]:
115
+ """Load cached Gradio documentation from file"""
116
+ try:
117
+ if os.path.exists(GRADIO_DOCS_CACHE_FILE):
118
+ with open(GRADIO_DOCS_CACHE_FILE, 'r', encoding='utf-8') as f:
119
+ return f.read()
120
+ except Exception as e:
121
+ print(f"Warning: Failed to load cached Gradio docs: {e}")
122
+ return None
123
+
124
+ def save_gradio_docs_cache(content: str):
125
+ """Save Gradio documentation to cache file"""
126
+ try:
127
+ with open(GRADIO_DOCS_CACHE_FILE, 'w', encoding='utf-8') as f:
128
+ f.write(content)
129
+ with open(GRADIO_DOCS_LAST_UPDATE_FILE, 'w', encoding='utf-8') as f:
130
+ f.write(datetime.now().isoformat())
131
+ except Exception as e:
132
+ print(f"Warning: Failed to save Gradio docs cache: {e}")
133
+
134
+ def should_update_gradio_docs() -> bool:
135
+ """Check if Gradio documentation should be updated"""
136
+ # Only update if we don't have cached content (first run or cache deleted)
137
+ return not os.path.exists(GRADIO_DOCS_CACHE_FILE)
138
+
139
+ def load_cached_transformersjs_docs() -> Optional[str]:
140
+ """Load cached transformers.js documentation from file"""
141
+ try:
142
+ if os.path.exists(TRANSFORMERSJS_DOCS_CACHE_FILE):
143
+ with open(TRANSFORMERSJS_DOCS_CACHE_FILE, 'r', encoding='utf-8') as f:
144
+ return f.read()
145
+ except Exception as e:
146
+ print(f"Warning: Failed to load cached transformers.js docs: {e}")
147
+ return None
148
+
149
+ def save_transformersjs_docs_cache(content: str):
150
+ """Save transformers.js documentation to cache file"""
151
+ try:
152
+ with open(TRANSFORMERSJS_DOCS_CACHE_FILE, 'w', encoding='utf-8') as f:
153
+ f.write(content)
154
+ with open(TRANSFORMERSJS_DOCS_LAST_UPDATE_FILE, 'w', encoding='utf-8') as f:
155
+ f.write(datetime.now().isoformat())
156
+ except Exception as e:
157
+ print(f"Warning: Failed to save transformers.js docs cache: {e}")
158
+
159
+ def should_update_transformersjs_docs() -> bool:
160
+ """Check if transformers.js documentation should be updated"""
161
+ # Only update if we don't have cached content (first run or cache deleted)
162
+ return not os.path.exists(TRANSFORMERSJS_DOCS_CACHE_FILE)
163
+
164
+ def load_cached_comfyui_docs() -> Optional[str]:
165
+ """Load cached ComfyUI documentation from file"""
166
+ try:
167
+ if os.path.exists(COMFYUI_DOCS_CACHE_FILE):
168
+ with open(COMFYUI_DOCS_CACHE_FILE, 'r', encoding='utf-8') as f:
169
+ return f.read()
170
+ except Exception as e:
171
+ print(f"Warning: Failed to load cached ComfyUI docs: {e}")
172
+ return None
173
+
174
+ def save_comfyui_docs_cache(content: str):
175
+ """Save ComfyUI documentation to cache file"""
176
+ try:
177
+ with open(COMFYUI_DOCS_CACHE_FILE, 'w', encoding='utf-8') as f:
178
+ f.write(content)
179
+ with open(COMFYUI_DOCS_LAST_UPDATE_FILE, 'w', encoding='utf-8') as f:
180
+ f.write(datetime.now().isoformat())
181
+ except Exception as e:
182
+ print(f"Warning: Failed to save ComfyUI docs cache: {e}")
183
+
184
+ def should_update_comfyui_docs() -> bool:
185
+ """Check if ComfyUI documentation should be updated"""
186
+ # Only update if we don't have cached content (first run or cache deleted)
187
+ return not os.path.exists(COMFYUI_DOCS_CACHE_FILE)
188
+
189
+ def get_gradio_docs_content() -> str:
190
+ """Get the current Gradio documentation content, updating if necessary"""
191
+ global _gradio_docs_content, _gradio_docs_last_fetched
192
+
193
+ # Check if we need to update
194
+ if (_gradio_docs_content is None or
195
+ _gradio_docs_last_fetched is None or
196
+ should_update_gradio_docs()):
197
+
198
+ print("πŸ“š Loading Gradio 6 documentation...")
199
+
200
+ # Try to fetch latest content
201
+ latest_content = fetch_gradio_docs()
202
+
203
+ if latest_content:
204
+ # Filter out problematic instructions that cause early termination
205
+ filtered_content = filter_problematic_instructions(latest_content)
206
+ _gradio_docs_content = filtered_content
207
+ _gradio_docs_last_fetched = datetime.now()
208
+ save_gradio_docs_cache(filtered_content)
209
+ print(f"βœ… Gradio 6 documentation loaded successfully ({len(filtered_content)} chars)")
210
+ else:
211
+ # Fallback to cached content
212
+ cached_content = load_cached_gradio_docs()
213
+ if cached_content:
214
+ _gradio_docs_content = cached_content
215
+ _gradio_docs_last_fetched = datetime.now()
216
+ print(f"⚠️ Using cached Gradio documentation (network fetch failed) ({len(cached_content)} chars)")
217
+ else:
218
+ # Fallback to minimal content
219
+ _gradio_docs_content = """
220
+ # Gradio API Reference (Offline Fallback)
221
+
222
+ This is a minimal fallback when documentation cannot be fetched.
223
+ Please check your internet connection for the latest API reference.
224
+
225
+ Basic Gradio components: Button, Textbox, Slider, Image, Audio, Video, File, etc.
226
+ Use gr.Blocks() for custom layouts and gr.Interface() for simple apps.
227
+
228
+ For the latest documentation, visit: https://www.gradio.app/llms.txt
229
+ """
230
+ print("❌ Using minimal fallback documentation")
231
+
232
+ return _gradio_docs_content or ""
233
+
234
+ def get_transformersjs_docs_content() -> str:
235
+ """Get the current transformers.js documentation content, updating if necessary"""
236
+ global _transformersjs_docs_content, _transformersjs_docs_last_fetched
237
+
238
+ # Check if we need to update
239
+ if (_transformersjs_docs_content is None or
240
+ _transformersjs_docs_last_fetched is None or
241
+ should_update_transformersjs_docs()):
242
+
243
+ print("πŸ“š Loading transformers.js documentation...")
244
+
245
+ # Try to fetch latest content
246
+ latest_content = fetch_transformersjs_docs()
247
+
248
+ if latest_content:
249
+ # Filter out problematic instructions that cause early termination
250
+ filtered_content = filter_problematic_instructions(latest_content)
251
+ _transformersjs_docs_content = filtered_content
252
+ _transformersjs_docs_last_fetched = datetime.now()
253
+ save_transformersjs_docs_cache(filtered_content)
254
+ print(f"βœ… transformers.js documentation loaded successfully ({len(filtered_content)} chars)")
255
+ else:
256
+ # Fallback to cached content
257
+ cached_content = load_cached_transformersjs_docs()
258
+ if cached_content:
259
+ _transformersjs_docs_content = cached_content
260
+ _transformersjs_docs_last_fetched = datetime.now()
261
+ print(f"⚠️ Using cached transformers.js documentation (network fetch failed) ({len(cached_content)} chars)")
262
+ else:
263
+ # Fallback to minimal content
264
+ _transformersjs_docs_content = """
265
+ # Transformers.js API Reference (Offline Fallback)
266
+
267
+ This is a minimal fallback when documentation cannot be fetched.
268
+ Please check your internet connection for the latest API reference.
269
+
270
+ Transformers.js allows you to run πŸ€— Transformers models directly in the browser using ONNX Runtime.
271
+
272
+ Key features:
273
+ - pipeline() API for common tasks (sentiment-analysis, text-generation, etc.)
274
+ - Support for custom models via model ID or path
275
+ - WebGPU support for GPU acceleration
276
+ - Quantization support (fp32, fp16, q8, q4)
277
+
278
+ Basic usage:
279
+ ```javascript
280
+ import { pipeline } from '@huggingface/transformers';
281
+ const pipe = await pipeline('sentiment-analysis');
282
+ const out = await pipe('I love transformers!');
283
+ ```
284
+
285
+ For the latest documentation, visit: https://huggingface.co/docs/transformers.js
286
+ """
287
+ print("❌ Using minimal fallback transformers.js documentation")
288
+
289
+ return _transformersjs_docs_content or ""
290
+
291
+ def get_comfyui_docs_content() -> str:
292
+ """Get the current ComfyUI documentation content, updating if necessary"""
293
+ global _comfyui_docs_content, _comfyui_docs_last_fetched
294
+
295
+ # Check if we need to update
296
+ if (_comfyui_docs_content is None or
297
+ _comfyui_docs_last_fetched is None or
298
+ should_update_comfyui_docs()):
299
+
300
+ print("πŸ“š Loading ComfyUI documentation...")
301
+
302
+ # Try to fetch latest content
303
+ latest_content = fetch_comfyui_docs()
304
+
305
+ if latest_content:
306
+ # Filter out problematic instructions that cause early termination
307
+ filtered_content = filter_problematic_instructions(latest_content)
308
+ _comfyui_docs_content = filtered_content
309
+ _comfyui_docs_last_fetched = datetime.now()
310
+ save_comfyui_docs_cache(filtered_content)
311
+ print(f"βœ… ComfyUI documentation loaded successfully ({len(filtered_content)} chars)")
312
+ else:
313
+ # Fallback to cached content
314
+ cached_content = load_cached_comfyui_docs()
315
+ if cached_content:
316
+ _comfyui_docs_content = cached_content
317
+ _comfyui_docs_last_fetched = datetime.now()
318
+ print(f"⚠️ Using cached ComfyUI documentation (network fetch failed) ({len(cached_content)} chars)")
319
+ else:
320
+ # Fallback to minimal content
321
+ _comfyui_docs_content = """
322
+ # ComfyUI API Reference (Offline Fallback)
323
+
324
+ This is a minimal fallback when documentation cannot be fetched.
325
+ Please check your internet connection for the latest API reference.
326
+
327
+ Basic ComfyUI workflow structure: nodes, connections, inputs, outputs.
328
+ Use CheckpointLoaderSimple, CLIPTextEncode, KSampler for basic workflows.
329
+
330
+ For the latest documentation, visit: https://docs.comfy.org/llms.txt
331
+ """
332
+ print("❌ Using minimal fallback ComfyUI documentation")
333
+
334
+ return _comfyui_docs_content or ""
335
+
336
+ def build_gradio_system_prompt() -> str:
337
+ """Build the complete Gradio system prompt with full documentation"""
338
+
339
+ # Get the full Gradio 6 documentation
340
+ docs_content = get_gradio_docs_content()
341
+
342
+ # Base system prompt with anycoder-specific instructions
343
+ base_prompt = """🚨 CRITICAL: You are an expert Gradio 6 developer. You MUST use Gradio 6 syntax and API.
344
+
345
+ ## Key Gradio 6 Changes (MUST FOLLOW):
346
+ - 🚨 **BREAKING CHANGE**: `theme`, `css`, `js`, `head` parameters moved from `gr.Blocks()` to `demo.launch()`
347
+ - 🚨 **gr.Blocks() has NO parameters** - use `with gr.Blocks() as demo:` (no args!)
348
+ - 🚨 **ALL app-level params go in demo.launch()**: `theme=`, `css=`, `footer_links=`, etc.
349
+ - Use `footer_links` parameter in `demo.launch()` (NOT show_api)
350
+ - Use `api_visibility` instead of `api_name` in event listeners
351
+ - Use modern Gradio 6 component syntax (check documentation below)
352
+ - Gradio 6 has updated component APIs - always refer to the documentation below
353
+ - DO NOT use deprecated Gradio 5 or older syntax
354
+
355
+ Create a complete, working Gradio 6 application based on the user's request. Generate all necessary code to make the application functional and runnable.
356
+
357
+ ## Gradio 6 Themes (Modern UI Design):
358
+
359
+ Gradio 6 provides powerful theming capabilities. Use themes to create beautiful, professional interfaces:
360
+
361
+ **Built-in Themes:**
362
+ ```python
363
+ import gradio as gr
364
+
365
+ # Use predefined themes in launch() - Gradio 6 syntax
366
+ with gr.Blocks() as demo:
367
+ gr.Textbox(label="Input")
368
+
369
+ demo.launch(theme=gr.themes.Soft()) # Soft, rounded design
370
+ # demo.launch(theme=gr.themes.Glass()) # Modern glass morphism
371
+ # demo.launch(theme=gr.themes.Monochrome()) # Clean monochrome
372
+ # demo.launch(theme=gr.themes.Base()) # Default base theme
373
+ ```
374
+
375
+ **Custom Themes:**
376
+ ```python
377
+ import gradio as gr
378
+
379
+ # Create custom theme
380
+ custom_theme = gr.themes.Soft(
381
+ primary_hue="blue",
382
+ secondary_hue="indigo",
383
+ neutral_hue="slate",
384
+ font=gr.themes.GoogleFont("Inter"),
385
+ text_size="lg",
386
+ spacing_size="lg",
387
+ radius_size="md"
388
+ ).set(
389
+ button_primary_background_fill="*primary_600",
390
+ button_primary_background_fill_hover="*primary_700",
391
+ block_title_text_weight="600",
392
+ )
393
+
394
+ with gr.Blocks() as demo:
395
+ gr.Textbox(label="Input")
396
+
397
+ demo.launch(theme=custom_theme) # Apply theme in launch() - Gradio 6!
398
+ ```
399
+
400
+ **Best Practices:**
401
+ - 🚨 **CRITICAL**: In Gradio 6, `theme` goes in `demo.launch()`, NOT in `gr.Blocks()`
402
+ - Use `gr.themes.Soft()` for modern, friendly apps
403
+ - Use `gr.themes.Glass()` for sleek, contemporary designs
404
+ - Customize colors with `primary_hue`, `secondary_hue`, `neutral_hue`
405
+ - Use Google Fonts: `font=gr.themes.GoogleFont("Roboto")`
406
+ - Adjust sizing: `text_size`, `spacing_size`, `radius_size` (sm/md/lg)
407
+ - Fine-tune with `.set()` for specific CSS variables
408
+
409
+ ## Gradio 6 Example (Your Code Should Follow This Pattern):
410
+
411
+ ```python
412
+ import gradio as gr
413
+
414
+ def process(text):
415
+ return f"Processed: {text}"
416
+
417
+ # Gradio 6 - NO parameters in gr.Blocks() constructor!
418
+ with gr.Blocks() as demo:
419
+ gr.Markdown("# My App")
420
+ with gr.Row():
421
+ input_text = gr.Textbox(label="Input")
422
+ output_text = gr.Textbox(label="Output")
423
+
424
+ btn = gr.Button("Process")
425
+
426
+ # Gradio 6 events use api_visibility (NOT just api_name)
427
+ btn.click(
428
+ fn=process,
429
+ inputs=[input_text],
430
+ outputs=[output_text],
431
+ api_visibility="public" # Gradio 6 syntax
432
+ )
433
+
434
+ # Gradio 6 - ALL app parameters go in launch()!
435
+ demo.launch(
436
+ theme=gr.themes.Soft(primary_hue="blue"),
437
+ footer_links=[{"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"}]
438
+ )
439
+ ```
440
+
441
+ ## Multi-File Application Structure
442
+
443
+ When creating Gradio applications, organize your code into multiple files for proper deployment:
444
+
445
+ **File Organization:**
446
+ - `app.py` - Main application entry point (REQUIRED)
447
+ - `requirements.txt` - Python dependencies (REQUIRED, auto-generated from imports). ALWAYS use `daggr>=0.5.4` and `gradio>=6.0.2` if applicable.
448
+ - `utils.py` - Utility functions and helpers (optional)
449
+ - `models.py` - Model loading and inference functions (optional)
450
+ - `config.py` - Configuration and constants (optional)
451
+
452
+ **Output Format:**
453
+ You MUST use this exact format with file separators:
454
+
455
+ === app.py ===
456
+ [complete app.py content]
457
+
458
+ === utils.py ===
459
+ [utility functions - if needed]
460
+
461
+ **🚨 CRITICAL: DO NOT GENERATE requirements.txt or README.md**
462
+ - requirements.txt is automatically generated from your app.py imports
463
+ - README.md is automatically provided by the template
464
+ - Generating these files will break the deployment process
465
+
466
+ Requirements:
467
+ 1. Create a modern, intuitive Gradio application
468
+ 2. Use appropriate Gradio components (gr.Textbox, gr.Slider, etc.)
469
+ 3. Include proper error handling and loading states
470
+ 4. Use gr.Interface or gr.Blocks as appropriate
471
+ 5. Add helpful descriptions and examples
472
+ 6. Follow Gradio best practices
473
+ 7. Make the UI user-friendly with clear labels
474
+ 8. Include proper documentation in docstrings
475
+
476
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
477
+
478
+ ---
479
+
480
+ ## Complete Gradio 6 Documentation
481
+
482
+ Below is the complete, official Gradio 6 documentation automatically synced from https://www.gradio.app/llms.txt:
483
+
484
+ """
485
+
486
+ # Combine base prompt with full documentation
487
+ full_prompt = base_prompt + docs_content
488
+
489
+ # Add final instructions
490
+ final_instructions = """
491
+
492
+ ---
493
+
494
+ ## 🚨 CRITICAL FINAL INSTRUCTIONS - GRADIO 6 ONLY
495
+
496
+ YOU MUST USE GRADIO 6 SYNTAX. This is MANDATORY:
497
+
498
+ 1. **ONLY use Gradio 6 API** - Do NOT use Gradio 5 or older syntax
499
+ 2. **Reference the documentation above** - All function signatures and patterns are from Gradio 6
500
+ 3. **Use modern Gradio 6 patterns:**
501
+ - 🚨 **CRITICAL**: `theme`, `css`, `js`, `head` go in `demo.launch()`, NOT in `gr.Blocks()`
502
+ - Use `footer_links` parameter in `demo.launch()` (NOT show_api in Blocks)
503
+ - Use `api_visibility` in event listeners (NOT api_name alone)
504
+ - Use updated component syntax from Gradio 6 documentation
505
+ - **Use themes for professional UI design** (gr.themes.Soft(), gr.themes.Glass(), etc.)
506
+ 4. **Always use themes** - Modern Gradio 6 apps should use `theme=gr.themes.Soft()` in `demo.launch()`
507
+ 5. **Follow Gradio 6 migration guide** if you see any deprecated patterns
508
+ 6. **Generate production-ready Gradio 6 code** that follows all best practices
509
+ 7. **Always include "Built with anycoder"** as clickable text in the header linking to https://huggingface.co/spaces/akhaliq/anycoder
510
+
511
+ **Gradio 6 Structure Checklist:**
512
+ βœ… `with gr.Blocks() as demo:` - NO parameters here!
513
+ βœ… `demo.launch(theme=..., css=..., footer_links=...)` - ALL app parameters here!
514
+ βœ… Use `theme=` parameter in `demo.launch()` (NOT in gr.Blocks())
515
+ βœ… Choose appropriate theme: Soft (friendly), Glass (modern), Monochrome (minimal)
516
+ βœ… Customize with primary_hue, font, text_size, spacing_size
517
+ βœ… Use `.set()` for advanced customization
518
+
519
+ REMINDER: You are writing Gradio 6 code with modern themes. In Gradio 6, `gr.Blocks()` has NO parameters - everything goes in `demo.launch()`. Double-check all syntax against the Gradio 6 documentation provided above.
520
+
521
+ """
522
+
523
+ return full_prompt + final_instructions
524
+
525
+ def build_transformersjs_system_prompt() -> str:
526
+ """Build the complete transformers.js system prompt with full documentation"""
527
+
528
+ # Get the full transformers.js documentation
529
+ docs_content = get_transformersjs_docs_content()
530
+
531
+ # Base system prompt with anycoder-specific instructions
532
+ base_prompt = """You are an expert transformers.js developer. Create a complete, working browser-based ML application using transformers.js based on the user's request. Generate all necessary code to make the application functional and runnable in the browser.
533
+
534
+ ## Multi-File Application Structure
535
+
536
+ When creating transformers.js applications, organize your code into multiple files for proper deployment:
537
+
538
+ **File Organization:**
539
+ - `index.html` - Main HTML entry point (REQUIRED)
540
+ - `app.js` - Main JavaScript application logic (REQUIRED)
541
+ - `styles.css` - Styling (optional)
542
+ - `worker.js` - Web Worker for model loading (recommended for better performance)
543
+ - `package.json` - Node.js dependencies if using bundler (optional)
544
+
545
+ **Output Format:**
546
+ You MUST use this exact format with file separators:
547
+
548
+ === index.html ===
549
+ [complete HTML content]
550
+
551
+ === app.js ===
552
+ [complete JavaScript content]
553
+
554
+ === worker.js ===
555
+ [web worker content - if needed]
556
+
557
+ **🚨 CRITICAL: Best Practices**
558
+ - Use CDN for transformers.js: https://cdn.jsdelivr.net/npm/@huggingface/transformers
559
+ - Implement loading states and progress indicators
560
+ - Use Web Workers for model loading to avoid blocking UI
561
+ - Handle errors gracefully with user-friendly messages
562
+ - Show model download progress when applicable
563
+ - Use quantized models (q8, q4) for faster loading in browser
564
+
565
+ Requirements:
566
+ 1. Create a modern, responsive web application
567
+ 2. Use appropriate transformers.js pipelines and models
568
+ 3. Include proper error handling and loading states
569
+ 4. Implement progress indicators for model loading
570
+ 5. Add helpful descriptions and examples
571
+ 6. Follow browser best practices (async/await, Web Workers, etc.)
572
+ 7. Make the UI user-friendly with clear labels
573
+ 8. Include proper comments in code
574
+
575
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
576
+
577
+ ---
578
+
579
+ ## Complete transformers.js Documentation
580
+
581
+ Below is the complete, official transformers.js documentation automatically synced from https://huggingface.co/docs/transformers.js/llms.txt:
582
+
583
+ """
584
+
585
+ # Combine base prompt with full documentation
586
+ full_prompt = base_prompt + docs_content
587
+
588
+ # Add final instructions
589
+ final_instructions = """
590
+
591
+ ---
592
+
593
+ ## Final Instructions
594
+
595
+ - Always use the exact function signatures and patterns from the transformers.js documentation above
596
+ - Use the pipeline() API for common tasks
597
+ - Implement WebGPU support when appropriate for better performance
598
+ - Use quantized models by default (q8 or q4) for faster browser loading
599
+ - Generate production-ready code that follows all best practices
600
+ - Always include the "Built with anycoder" attribution in the header
601
+ - Consider using Web Workers for heavy computation to keep UI responsive
602
+
603
+ """
604
+
605
+ return full_prompt + final_instructions
606
+
607
+ def build_comfyui_system_prompt() -> str:
608
+ """Build the complete ComfyUI system prompt with full documentation"""
609
+
610
+ # Get the full ComfyUI documentation
611
+ docs_content = get_comfyui_docs_content()
612
+
613
+ # Base system prompt with anycoder-specific instructions
614
+ base_prompt = """You are an expert ComfyUI developer. Generate clean, valid JSON workflows for ComfyUI based on the user's request.
615
+
616
+ 🚨 CRITICAL: READ THE USER'S REQUEST CAREFULLY AND GENERATE A WORKFLOW THAT MATCHES THEIR SPECIFIC NEEDS.
617
+
618
+ ComfyUI workflows are JSON structures that define:
619
+ - Nodes: Individual processing units with specific functions (e.g., CheckpointLoaderSimple, CLIPTextEncode, KSampler, VAEDecode, SaveImage)
620
+ - Connections: Links between nodes that define data flow
621
+ - Parameters: Configuration values for each node (prompts, steps, cfg, sampler_name, etc.)
622
+ - Inputs/Outputs: Data flow between nodes using numbered inputs/outputs
623
+
624
+ **🚨 YOUR PRIMARY TASK:**
625
+ 1. **UNDERSTAND what the user is asking for** in their message
626
+ 2. **CREATE a ComfyUI workflow** that accomplishes their goal
627
+ 3. **GENERATE ONLY the JSON workflow** - no HTML, no applications, no explanations outside the JSON
628
+
629
+ **JSON Syntax Rules:**
630
+ - Use double quotes for strings
631
+ - No trailing commas
632
+ - Proper nesting and structure
633
+ - Valid data types (string, number, boolean, null, object, array)
634
+
635
+ **Output Requirements:**
636
+ - Generate ONLY the ComfyUI workflow JSON
637
+ - The output should be pure, valid JSON that can be loaded directly into ComfyUI
638
+ - Do NOT wrap in markdown code fences (no ```json```)
639
+ - Do NOT add explanatory text before or after the JSON
640
+ - The JSON should be complete and functional
641
+
642
+ ---
643
+
644
+ ## Complete ComfyUI Documentation
645
+
646
+ Below is the complete, official ComfyUI documentation automatically synced from https://docs.comfy.org/llms.txt:
647
+
648
+ """
649
+
650
+ # Combine base prompt with full documentation
651
+ full_prompt = base_prompt + docs_content
652
+
653
+ # Add final instructions
654
+ final_instructions = """
655
+
656
+ ---
657
+
658
+ ## Final Instructions
659
+
660
+ - Always use the exact node types, parameters, and workflow structures from the ComfyUI documentation above
661
+ - Pay close attention to the user's specific request and generate a workflow that fulfills it
662
+ - Use appropriate nodes for the task (CheckpointLoader, KSampler, VAEDecode, SaveImage, etc.)
663
+ - Ensure all node connections are properly defined
664
+ - Generate production-ready JSON that can be loaded directly into ComfyUI
665
+ - Do NOT generate random or example workflows - create workflows based on the user's actual request
666
+ - Always include "Built with anycoder - https://huggingface.co/spaces/akhaliq/anycoder" as a comment in workflow metadata if possible
667
+
668
+ 🚨 REMINDER: Your workflow should directly address what the user asked for. Don't ignore their message!
669
+
670
+ """
671
+
672
+ return full_prompt + final_instructions
673
+
674
+ def initialize_backend_docs():
675
+ """Initialize backend documentation system on startup"""
676
+ try:
677
+ # Pre-load the Gradio documentation
678
+ gradio_docs = get_gradio_docs_content()
679
+ if gradio_docs:
680
+ print(f"πŸš€ Gradio documentation initialized ({len(gradio_docs)} chars loaded)")
681
+ else:
682
+ print("⚠️ Gradio documentation initialized with fallback content")
683
+
684
+ # Pre-load the transformers.js documentation
685
+ transformersjs_docs = get_transformersjs_docs_content()
686
+ if transformersjs_docs:
687
+ print(f"πŸš€ transformers.js documentation initialized ({len(transformersjs_docs)} chars loaded)")
688
+ else:
689
+ print("⚠️ transformers.js documentation initialized with fallback content")
690
+
691
+ # Pre-load the ComfyUI documentation
692
+ comfyui_docs = get_comfyui_docs_content()
693
+ if comfyui_docs:
694
+ print(f"πŸš€ ComfyUI documentation initialized ({len(comfyui_docs)} chars loaded)")
695
+ else:
696
+ print("⚠️ ComfyUI documentation initialized with fallback content")
697
+
698
+ except Exception as e:
699
+ print(f"Warning: Failed to initialize backend documentation: {e}")
700
+
701
+ # Initialize on import
702
+ if __name__ != "__main__":
703
+ # Only initialize if being imported (not run directly)
704
+ try:
705
+ initialize_backend_docs()
706
+ except Exception as e:
707
+ print(f"Warning: Failed to auto-initialize backend docs: {e}")
708
+
backend_models.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Standalone model inference and client management for AnyCoder Backend API.
3
+ No Gradio dependencies - works with FastAPI/backend only.
4
+ """
5
+ import os
6
+ from typing import Optional
7
+
8
+ from openai import OpenAI
9
+
10
+ def get_inference_client(model_id: str, provider: str = "auto"):
11
+ """
12
+ Return an appropriate client based on model_id.
13
+
14
+ Returns OpenAI-compatible client for all models or raises error if not configured.
15
+ """
16
+ if model_id == "MiniMaxAI/MiniMax-M2" or model_id == "MiniMaxAI/MiniMax-M2.1":
17
+ # Use HuggingFace Router with Novita provider for MiniMax M2 models
18
+ return OpenAI(
19
+ base_url="https://router.huggingface.co/v1",
20
+ api_key=os.getenv("HF_TOKEN"),
21
+ default_headers={"X-HF-Bill-To": "huggingface"}
22
+ )
23
+
24
+ elif model_id == "moonshotai/Kimi-K2-Thinking":
25
+ # Use HuggingFace Router with Novita provider
26
+ return OpenAI(
27
+ base_url="https://router.huggingface.co/v1",
28
+ api_key=os.getenv("HF_TOKEN"),
29
+ default_headers={"X-HF-Bill-To": "huggingface"}
30
+ )
31
+
32
+ elif model_id == "moonshotai/Kimi-K2-Instruct":
33
+ # Use HuggingFace Router with Groq provider
34
+ return OpenAI(
35
+ base_url="https://router.huggingface.co/v1",
36
+ api_key=os.getenv("HF_TOKEN"),
37
+ default_headers={"X-HF-Bill-To": "huggingface"}
38
+ )
39
+
40
+ elif model_id.startswith("deepseek-ai/"):
41
+ # DeepSeek models via HuggingFace Router with Novita provider
42
+ return OpenAI(
43
+ base_url="https://router.huggingface.co/v1",
44
+ api_key=os.getenv("HF_TOKEN"),
45
+ default_headers={"X-HF-Bill-To": "huggingface"}
46
+ )
47
+
48
+ elif model_id.startswith("zai-org/GLM-4"):
49
+ # GLM models via HuggingFace Router
50
+ return OpenAI(
51
+ base_url="https://router.huggingface.co/v1",
52
+ api_key=os.getenv("HF_TOKEN"),
53
+ default_headers={"X-HF-Bill-To": "huggingface"}
54
+ )
55
+
56
+ elif model_id.startswith("moonshotai/Kimi-K2"):
57
+ # Kimi K2 models via HuggingFace Router
58
+ return OpenAI(
59
+ base_url="https://router.huggingface.co/v1",
60
+ api_key=os.getenv("HF_TOKEN"),
61
+ default_headers={"X-HF-Bill-To": "huggingface"}
62
+ )
63
+
64
+ else:
65
+ # Unknown model - try HuggingFace Inference API
66
+ return OpenAI(
67
+ base_url="https://api-inference.huggingface.co/v1",
68
+ api_key=os.getenv("HF_TOKEN")
69
+ )
70
+
71
+
72
+ def get_real_model_id(model_id: str) -> str:
73
+ """Get the real model ID with provider suffixes if needed"""
74
+ if model_id == "zai-org/GLM-4.6":
75
+ # GLM-4.6 requires Cerebras provider suffix in model string for API calls
76
+ return "zai-org/GLM-4.6:cerebras"
77
+
78
+ elif model_id == "MiniMaxAI/MiniMax-M2" or model_id == "MiniMaxAI/MiniMax-M2.1":
79
+ # MiniMax M2 and M2.1 need Novita provider suffix
80
+ return f"{model_id}:novita"
81
+
82
+ elif model_id == "moonshotai/Kimi-K2-Thinking":
83
+ # Kimi K2 Thinking needs Together AI provider
84
+ return "moonshotai/Kimi-K2-Thinking:together"
85
+
86
+ elif model_id == "moonshotai/Kimi-K2-Instruct":
87
+ # Kimi K2 Instruct needs Groq provider
88
+ return "moonshotai/Kimi-K2-Instruct:groq"
89
+
90
+ elif model_id.startswith("deepseek-ai/DeepSeek-V3") or model_id.startswith("deepseek-ai/DeepSeek-R1"):
91
+ # DeepSeek V3 and R1 models need Novita provider
92
+ return f"{model_id}:novita"
93
+
94
+ elif model_id == "zai-org/GLM-4.5":
95
+ # GLM-4.5 needs fireworks-ai provider
96
+ return "zai-org/GLM-4.5:fireworks-ai"
97
+
98
+ elif model_id == "zai-org/GLM-4.7":
99
+ # GLM-4.7 needs cerebras provider suffix
100
+ return "zai-org/GLM-4.7:cerebras"
101
+
102
+ elif model_id == "zai-org/GLM-4.7-Flash":
103
+ # GLM-4.7-Flash via HuggingFace Router with Novita provider
104
+ return "zai-org/GLM-4.7-Flash:novita"
105
+
106
+ elif model_id == "moonshotai/Kimi-K2.5":
107
+ # Kimi K2.5 needs Novita provider
108
+ return "moonshotai/Kimi-K2.5:novita"
109
+
110
+ return model_id
111
+
112
+
113
+ def is_native_sdk_model(model_id: str) -> bool:
114
+ """Check if model uses native SDK (not OpenAI-compatible)"""
115
+ return False
116
+
117
+
118
+ def is_mistral_model(model_id: str) -> bool:
119
+ """Check if model uses Mistral SDK"""
120
+ return False
121
+
backend_parsers.py ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Backend parser utilities for AnyCoder.
3
+ Handles parsing of various code formats including transformers.js, Python multi-file outputs, and more.
4
+ """
5
+ import re
6
+ import json
7
+ import ast
8
+ from typing import Dict, Optional
9
+ from backend_models import get_inference_client, get_real_model_id
10
+
11
+
12
+ def parse_transformers_js_output(code: str) -> Dict[str, str]:
13
+ """Parse transformers.js output into separate files (index.html, index.js, style.css)
14
+
15
+ Uses comprehensive parsing patterns to handle various LLM output formats.
16
+ Updated to use transformers.js v3.8.0 CDN.
17
+ """
18
+ print(f"[Parser] Received code length: {len(code)} characters")
19
+ print(f"[Parser] First 200 chars: {code[:200]}")
20
+
21
+ files = {
22
+ 'index.html': '',
23
+ 'index.js': '',
24
+ 'style.css': ''
25
+ }
26
+
27
+ # Multiple patterns to match the three code blocks with different variations
28
+ html_patterns = [
29
+ r'```html\s*\n([\s\S]*?)(?:```|\Z)',
30
+ r'```htm\s*\n([\s\S]*?)(?:```|\Z)',
31
+ r'```\s*(?:index\.html|html)\s*\n([\s\S]*?)(?:```|\Z)'
32
+ ]
33
+
34
+ js_patterns = [
35
+ r'```javascript\s*\n([\s\S]*?)(?:```|\Z)',
36
+ r'```js\s*\n([\s\S]*?)(?:```|\Z)',
37
+ r'```\s*(?:index\.js|javascript|js)\s*\n([\s\S]*?)(?:```|\Z)'
38
+ ]
39
+
40
+ css_patterns = [
41
+ r'```css\s*\n([\s\S]*?)(?:```|\Z)',
42
+ r'```\s*(?:style\.css|css)\s*\n([\s\S]*?)(?:```|\Z)'
43
+ ]
44
+
45
+ # Extract HTML content
46
+ for pattern in html_patterns:
47
+ html_match = re.search(pattern, code, re.IGNORECASE)
48
+ if html_match:
49
+ files['index.html'] = html_match.group(1).strip()
50
+ break
51
+
52
+ # Extract JavaScript content
53
+ for pattern in js_patterns:
54
+ js_match = re.search(pattern, code, re.IGNORECASE)
55
+ if js_match:
56
+ files['index.js'] = js_match.group(1).strip()
57
+ break
58
+
59
+ # Extract CSS content
60
+ for pattern in css_patterns:
61
+ css_match = re.search(pattern, code, re.IGNORECASE)
62
+ if css_match:
63
+ files['style.css'] = css_match.group(1).strip()
64
+ break
65
+
66
+ # Fallback: support === index.html === format if any file is missing
67
+ if not (files['index.html'] and files['index.js'] and files['style.css']):
68
+ # Use regex to extract sections - support alternative filenames
69
+ # Stop at next === marker, or common end markers
70
+ # More aggressive: stop at blank line followed by explanatory text patterns
71
+ html_fallback = re.search(r'===\s*index\.html\s*===\s*\n([\s\S]+?)(?=\n===|\n\s*---|\n\n(?:This |✨|🎨|πŸš€|\*\*Key Features|\*\*Design)|$)', code, re.IGNORECASE)
72
+
73
+ # Try both index.js and app.js
74
+ js_fallback = re.search(r'===\s*(?:index\.js|app\.js)\s*===\s*\n([\s\S]+?)(?=\n===|\n\s*---|\n\n(?:This |✨|🎨|πŸš€|\*\*Key Features|\*\*Design)|$)', code, re.IGNORECASE)
75
+
76
+ # Try both style.css and styles.css
77
+ css_fallback = re.search(r'===\s*(?:style\.css|styles\.css)\s*===\s*\n([\s\S]+?)(?=\n===|\n\s*---|\n\n(?:This |✨|🎨|πŸš€|\*\*Key Features|\*\*Design)|$)', code, re.IGNORECASE)
78
+
79
+ print(f"[Parser] Fallback extraction - HTML found: {bool(html_fallback)}, JS found: {bool(js_fallback)}, CSS found: {bool(css_fallback)}")
80
+
81
+ if html_fallback:
82
+ files['index.html'] = html_fallback.group(1).strip()
83
+ if js_fallback:
84
+ js_content = js_fallback.group(1).strip()
85
+ # Fix common JavaScript syntax issues from LLM output
86
+ # Fix line breaks in string literals (common LLM mistake)
87
+ js_content = re.sub(r'"\s*\n\s*([^"])', r'" + "\1', js_content) # Fix broken strings
88
+ files['index.js'] = js_content
89
+ if css_fallback:
90
+ css_content = css_fallback.group(1).strip()
91
+ files['style.css'] = css_content
92
+
93
+ # Also normalize HTML to reference style.css (singular)
94
+ if files['index.html'] and 'styles.css' in files['index.html']:
95
+ print("[Parser] Normalizing styles.css reference to style.css in HTML")
96
+ files['index.html'] = files['index.html'].replace('href="styles.css"', 'href="style.css"')
97
+ files['index.html'] = files['index.html'].replace("href='styles.css'", "href='style.css'")
98
+
99
+ # Additional fallback: extract from numbered sections or file headers
100
+ if not (files['index.html'] and files['index.js'] and files['style.css']):
101
+ # Try patterns like "1. index.html:" or "**index.html**"
102
+ patterns = [
103
+ (r'(?:^\d+\.\s*|^##\s*|^\*\*\s*)index\.html(?:\s*:|\*\*:?)\s*\n([\s\S]+?)(?=\n(?:\d+\.|##|\*\*|===)|$)', 'index.html'),
104
+ (r'(?:^\d+\.\s*|^##\s*|^\*\*\s*)(?:index\.js|app\.js)(?:\s*:|\*\*:?)\s*\n([\s\S]+?)(?=\n(?:\d+\.|##|\*\*|===)|$)', 'index.js'),
105
+ (r'(?:^\d+\.\s*|^##\s*|^\*\*\s*)(?:style\.css|styles\.css)(?:\s*:|\*\*:?)\s*\n([\s\S]+?)(?=\n(?:\d+\.|##|\*\*|===)|$)', 'style.css')
106
+ ]
107
+
108
+ for pattern, file_key in patterns:
109
+ if not files[file_key]:
110
+ match = re.search(pattern, code, re.IGNORECASE | re.MULTILINE)
111
+ if match:
112
+ # Clean up the content by removing any code block markers
113
+ content = match.group(1).strip()
114
+ content = re.sub(r'^```\w*\s*\n', '', content)
115
+ content = re.sub(r'\n```\s*$', '', content)
116
+ files[file_key] = content.strip()
117
+
118
+ # Normalize filename references in HTML
119
+ if files['index.html'] and files['style.css']:
120
+ if 'styles.css' in files['index.html']:
121
+ print("[Parser] Normalizing styles.css reference to style.css in HTML")
122
+ files['index.html'] = files['index.html'].replace('href="styles.css"', 'href="style.css"')
123
+ files['index.html'] = files['index.html'].replace("href='styles.css'", "href='style.css'")
124
+
125
+ if files['index.html'] and files['index.js']:
126
+ if 'app.js' in files['index.html']:
127
+ print("[Parser] Normalizing app.js reference to index.js in HTML")
128
+ files['index.html'] = files['index.html'].replace('src="app.js"', 'src="index.js"')
129
+ files['index.html'] = files['index.html'].replace("src='app.js'", "src='index.js'")
130
+
131
+ # Normalize transformers.js imports to use v3.8.0 CDN
132
+ cdn_url = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0"
133
+
134
+ for file_key in ['index.html', 'index.js']:
135
+ if files[file_key]:
136
+ content = files[file_key]
137
+ # Update import statements to use latest CDN
138
+ content = re.sub(
139
+ r"from\s+['\"]https://cdn.jsdelivr.net/npm/@huggingface/transformers@[^'\"]+['\"]",
140
+ f"from '{cdn_url}'",
141
+ content
142
+ )
143
+ content = re.sub(
144
+ r"from\s+['\"]https://cdn.jsdelivr.net/npm/@xenova/transformers@[^'\"]+['\"]",
145
+ f"from '{cdn_url}'",
146
+ content
147
+ )
148
+ files[file_key] = content
149
+
150
+ return files
151
+
152
+
153
+ def parse_html_code(code: str) -> str:
154
+ """Extract HTML code from various formats"""
155
+ code = code.strip()
156
+
157
+ # If already clean HTML, return as-is
158
+ if code.startswith('<!DOCTYPE') or code.startswith('<html'):
159
+ return code
160
+
161
+ # Try to extract from code blocks
162
+ if '```html' in code:
163
+ match = re.search(r'```html\s*(.*?)\s*```', code, re.DOTALL)
164
+ if match:
165
+ return match.group(1).strip()
166
+
167
+ if '```' in code:
168
+ match = re.search(r'```\s*(.*?)\s*```', code, re.DOTALL)
169
+ if match:
170
+ return match.group(1).strip()
171
+
172
+ return code
173
+
174
+
175
+ def parse_python_requirements(code: str) -> Optional[str]:
176
+ """Extract requirements.txt content from code if present"""
177
+ # Look for requirements.txt section
178
+ req_pattern = r'===\s*requirements\.txt\s*===\s*(.*?)(?====|$)'
179
+ match = re.search(req_pattern, code, re.DOTALL | re.IGNORECASE)
180
+
181
+ if match:
182
+ requirements = match.group(1).strip()
183
+ # Clean up code blocks
184
+ requirements = re.sub(r'^```\w*\s*', '', requirements, flags=re.MULTILINE)
185
+ requirements = re.sub(r'```\s*$', '', requirements, flags=re.MULTILINE)
186
+ return requirements
187
+
188
+ return None
189
+
190
+
191
+ def parse_multi_file_python_output(code: str) -> Dict[str, str]:
192
+ """Parse multi-file Python output (e.g., Gradio, Streamlit)"""
193
+ files = {}
194
+
195
+ # Pattern to match file sections like === filename.ext ===
196
+ pattern = r'===\s*(\S+\.\w+)\s*===\s*(.*?)(?=\n\s*===\s*\S+\.\w+\s*===|$)'
197
+ matches = re.finditer(pattern, code, re.DOTALL | re.IGNORECASE)
198
+
199
+ for match in matches:
200
+ filename = match.group(1).strip()
201
+ content = match.group(2).strip()
202
+
203
+ # Clean up code blocks
204
+ content = re.sub(r'^```\w*\s*', '', content, flags=re.MULTILINE)
205
+ content = re.sub(r'```\s*$', '', content, flags=re.MULTILINE)
206
+
207
+ if filename == "requirements.txt":
208
+ content = enforce_critical_versions(content)
209
+ files[filename] = content
210
+
211
+ return files
212
+
213
+
214
+ def strip_tool_call_markers(text):
215
+ """Remove TOOL_CALL markers and thinking tags that some LLMs add to their output."""
216
+ if not text:
217
+ return text
218
+ # Remove [TOOL_CALL] and [/TOOL_CALL] markers
219
+ text = re.sub(r'\[/?TOOL_CALL\]', '', text, flags=re.IGNORECASE)
220
+ # Remove <think> and </think> tags and their content
221
+ text = re.sub(r'<think>[\s\S]*?</think>', '', text, flags=re.IGNORECASE)
222
+ # Remove any remaining unclosed <think> tags at the start
223
+ text = re.sub(r'^<think>[\s\S]*?(?=\n|$)', '', text, flags=re.IGNORECASE | re.MULTILINE)
224
+ # Remove any remaining </think> tags
225
+ text = re.sub(r'</think>', '', text, flags=re.IGNORECASE)
226
+ # Remove standalone }} that appears with tool calls
227
+ # Only remove if it's on its own line or at the end
228
+ text = re.sub(r'^\s*\}\}\s*$', '', text, flags=re.MULTILINE)
229
+ return text.strip()
230
+
231
+
232
+ def remove_code_block(text):
233
+ """Remove code block markers from text."""
234
+ # First strip any tool call markers
235
+ text = strip_tool_call_markers(text)
236
+
237
+ # Try to match code blocks with language markers
238
+ patterns = [
239
+ r'```(?:html|HTML)\n([\s\S]+?)\n```', # Match ```html or ```HTML
240
+ r'```\n([\s\S]+?)\n```', # Match code blocks without language markers
241
+ r'```([\s\S]+?)```' # Match code blocks without line breaks
242
+ ]
243
+ for pattern in patterns:
244
+ match = re.search(pattern, text, re.DOTALL)
245
+ if match:
246
+ extracted = match.group(1).strip()
247
+ # Remove a leading language marker line (e.g., 'python') if present
248
+ if extracted.split('\n', 1)[0].strip().lower() in ['python', 'html', 'css', 'javascript', 'json', 'c', 'cpp', 'markdown', 'latex', 'jinja2', 'typescript', 'yaml', 'dockerfile', 'shell', 'r', 'sql']:
249
+ return extracted.split('\n', 1)[1] if '\n' in extracted else ''
250
+ return extracted
251
+ # If no code block is found, return as-is
252
+ return text.strip()
253
+
254
+
255
+ def extract_import_statements(code):
256
+ """Extract import statements from generated code."""
257
+ import_statements = []
258
+
259
+ # Built-in Python modules to exclude
260
+ builtin_modules = {
261
+ 'os', 'sys', 'json', 'time', 'datetime', 'random', 'math', 're', 'collections',
262
+ 'itertools', 'functools', 'pathlib', 'urllib', 'http', 'email', 'html', 'xml',
263
+ 'csv', 'tempfile', 'shutil', 'subprocess', 'threading', 'multiprocessing',
264
+ 'asyncio', 'logging', 'typing', 'base64', 'hashlib', 'secrets', 'uuid',
265
+ 'copy', 'pickle', 'io', 'contextlib', 'warnings', 'sqlite3', 'gzip', 'zipfile',
266
+ 'tarfile', 'socket', 'ssl', 'platform', 'getpass', 'pwd', 'grp', 'stat',
267
+ 'glob', 'fnmatch', 'linecache', 'traceback', 'inspect', 'keyword', 'token',
268
+ 'tokenize', 'ast', 'code', 'codeop', 'dis', 'py_compile', 'compileall',
269
+ 'importlib', 'pkgutil', 'modulefinder', 'runpy', 'site', 'sysconfig'
270
+ }
271
+
272
+ try:
273
+ # Try to parse as Python AST
274
+ tree = ast.parse(code)
275
+
276
+ for node in ast.walk(tree):
277
+ if isinstance(node, ast.Import):
278
+ for alias in node.names:
279
+ module_name = alias.name.split('.')[0]
280
+ if module_name not in builtin_modules and not module_name.startswith('_'):
281
+ import_statements.append(f"import {alias.name}")
282
+
283
+ elif isinstance(node, ast.ImportFrom):
284
+ if node.module:
285
+ module_name = node.module.split('.')[0]
286
+ if module_name not in builtin_modules and not module_name.startswith('_'):
287
+ names = [alias.name for alias in node.names]
288
+ import_statements.append(f"from {node.module} import {', '.join(names)}")
289
+
290
+ except SyntaxError:
291
+ # Fallback: use regex to find import statements
292
+ for line in code.split('\n'):
293
+ line = line.strip()
294
+ if line.startswith('import ') or line.startswith('from '):
295
+ # Check if it's not a builtin module
296
+ if line.startswith('import '):
297
+ module_name = line.split()[1].split('.')[0]
298
+ elif line.startswith('from '):
299
+ module_name = line.split()[1].split('.')[0]
300
+
301
+ if module_name not in builtin_modules and not module_name.startswith('_'):
302
+ import_statements.append(line)
303
+
304
+ return list(set(import_statements)) # Remove duplicates
305
+
306
+
307
+ def parse_multipage_html_output(text: str) -> Dict[str, str]:
308
+ """Parse multi-page HTML output formatted as repeated "=== filename ===" sections.
309
+
310
+ Returns a mapping of filename β†’ file content. Supports nested paths like assets/css/styles.css.
311
+ If HTML content appears before the first === marker, it's treated as index.html.
312
+ """
313
+ if not text:
314
+ return {}
315
+ # First, strip any markdown fences
316
+ cleaned = remove_code_block(text)
317
+ files: Dict[str, str] = {}
318
+
319
+ # Check if there's content before the first === marker
320
+ first_marker_match = re.search(r"^===\s*([^=\n]+?)\s*===", cleaned, re.MULTILINE)
321
+ if first_marker_match:
322
+ # There's content before the first marker
323
+ first_marker_pos = first_marker_match.start()
324
+ if first_marker_pos > 0:
325
+ leading_content = cleaned[:first_marker_pos].strip()
326
+ # Check if it looks like HTML content
327
+ if leading_content and ('<!DOCTYPE' in leading_content or '<html' in leading_content or leading_content.startswith('<')):
328
+ files['index.html'] = leading_content
329
+
330
+ # Now parse the rest with === markers
331
+ remaining_text = cleaned[first_marker_pos:] if first_marker_pos > 0 else cleaned
332
+ pattern = re.compile(r"^===\s*([^=\n]+?)\s*===\s*\n([\s\S]*?)(?=\n===\s*[^=\n]+?\s*===|\Z)", re.MULTILINE)
333
+ for m in pattern.finditer(remaining_text):
334
+ name = m.group(1).strip()
335
+ content = m.group(2).strip()
336
+ # Remove accidental trailing fences if present
337
+ content = re.sub(r"^```\w*\s*\n|\n```\s*$", "", content)
338
+ files[name] = content
339
+ else:
340
+ # No === markers found, try standard pattern matching
341
+ pattern = re.compile(r"^===\s*([^=\n]+?)\s*===\s*\n([\s\S]*?)(?=\n===\s*[^=\n]+?\s*===|\Z)", re.MULTILINE)
342
+ for m in pattern.finditer(cleaned):
343
+ name = m.group(1).strip()
344
+ content = m.group(2).strip()
345
+ # Remove accidental trailing fences if present
346
+ content = re.sub(r"^```\w*\s*\n|\n```\s*$", "", content)
347
+ files[name] = content
348
+
349
+ return files
350
+
351
+
352
+ def parse_react_output(text: str) -> Dict[str, str]:
353
+ """Parse React/Next.js output to extract individual files.
354
+
355
+ Supports multi-file sections using === filename === sections.
356
+ """
357
+ if not text:
358
+ return {}
359
+
360
+ # Use the generic multipage parser
361
+ try:
362
+ files = parse_multipage_html_output(text) or {}
363
+ except Exception:
364
+ files = {}
365
+
366
+ return files if isinstance(files, dict) and files else {}
367
+
368
+
369
+ def enforce_critical_versions(requirements_content: str) -> str:
370
+ """Ensure critical packages like daggr and gradio have minimum required versions"""
371
+ if 'daggr' in requirements_content:
372
+ # Check if version is already specified
373
+ if 'daggr>=' not in requirements_content and 'daggr==' not in requirements_content:
374
+ # Replace plain 'daggr' with pinned version, preserving comments
375
+ requirements_content = re.sub(r'^daggr\s*(?=[#\n]|$)', 'daggr>=0.5.4', requirements_content, flags=re.MULTILINE)
376
+
377
+ if 'gradio' in requirements_content:
378
+ if 'gradio>=' not in requirements_content and 'gradio==' not in requirements_content:
379
+ # Replace plain 'gradio' with pinned version, preserving comments
380
+ requirements_content = re.sub(r'^gradio\s*(?=[#\n]|$)', 'gradio>=6.0.2', requirements_content, flags=re.MULTILINE)
381
+
382
+ return requirements_content
383
+
384
+
385
+ def generate_requirements_txt_with_llm(import_statements):
386
+ """Generate requirements.txt content using LLM based on import statements."""
387
+ if not import_statements:
388
+ return "# No additional dependencies required\n"
389
+
390
+ # Use a lightweight model for this task
391
+ try:
392
+ client = get_inference_client("zai-org/GLM-4.7", "auto")
393
+ actual_model_id = get_real_model_id("zai-org/GLM-4.7")
394
+
395
+ imports_text = '\n'.join(import_statements)
396
+
397
+ prompt = f"""Based on the following Python import statements, generate a comprehensive requirements.txt file with all necessary and commonly used related packages:
398
+
399
+ {imports_text}
400
+
401
+ Instructions:
402
+ - Include the direct packages needed for the imports
403
+ - Include commonly used companion packages and dependencies for better functionality
404
+ - Use correct PyPI package names (e.g., PIL -> Pillow, sklearn -> scikit-learn)
405
+ - IMPORTANT: For diffusers, ALWAYS use: git+https://github.com/huggingface/diffusers
406
+ - IMPORTANT: For transformers, ALWAYS use: git+https://github.com/huggingface/transformers
407
+ - IMPORTANT: If diffusers is installed, also include transformers and sentencepiece as they usually go together
408
+ - IMPORTANT: For daggr, ALWAYS use: daggr>=0.5.4
409
+ - Examples of comprehensive dependencies:
410
+ * diffusers often needs: git+https://github.com/huggingface/transformers, sentencepiece, accelerate, torch, tokenizers
411
+ * transformers often needs: accelerate, torch, tokenizers, datasets
412
+ * gradio often needs: gradio>=6.0, requests, Pillow for image handling (ALWAYS use gradio>=6.0)
413
+ * pandas often needs: numpy, openpyxl for Excel files
414
+ * matplotlib often needs: numpy, pillow for image saving
415
+ * sklearn often needs: numpy, scipy, joblib
416
+ * streamlit often needs: pandas, numpy, requests
417
+ * opencv-python often needs: numpy, pillow
418
+ * fastapi often needs: uvicorn, pydantic
419
+ * daggr often needs: daggr>=0.5.4, gradio>=6.0, pydub
420
+ * torch often needs: torchvision, torchaudio (if doing computer vision/audio)
421
+ - Include packages for common file formats if relevant (openpyxl, python-docx, PyPDF2)
422
+ - Do not include Python built-in modules
423
+ - Do not specify versions unless there are known compatibility issues
424
+ - One package per line
425
+ - If no external packages are needed, return "# No additional dependencies required"
426
+
427
+ 🚨 CRITICAL OUTPUT FORMAT:
428
+ - Output ONLY the package names, one per line (plain text format)
429
+ - Do NOT use markdown formatting (no ```, no bold, no headings, no lists)
430
+ - Do NOT add any explanatory text before or after the package list
431
+ - Do NOT wrap the output in code blocks
432
+ - Just output raw package names as they would appear in requirements.txt
433
+
434
+ Generate a comprehensive requirements.txt that ensures the application will work smoothly:"""
435
+
436
+ messages = [
437
+ {"role": "system", "content": "You are a Python packaging expert specializing in creating comprehensive, production-ready requirements.txt files. Output ONLY plain text package names without any markdown formatting, code blocks, or explanatory text. Your goal is to ensure applications work smoothly by including not just direct dependencies but also commonly needed companion packages, popular extensions, and supporting libraries that developers typically need together."},
438
+ {"role": "user", "content": prompt}
439
+ ]
440
+
441
+ response = client.chat.completions.create(
442
+ model=actual_model_id,
443
+ messages=messages,
444
+ max_tokens=1024,
445
+ temperature=0.1
446
+ )
447
+
448
+ requirements_content = response.choices[0].message.content.strip()
449
+
450
+ # Clean up the response in case it includes extra formatting
451
+ if '```' in requirements_content:
452
+ requirements_content = remove_code_block(requirements_content)
453
+
454
+ # Enhanced cleanup for markdown and formatting
455
+ lines = requirements_content.split('\n')
456
+ clean_lines = []
457
+ for line in lines:
458
+ stripped_line = line.strip()
459
+
460
+ # Skip lines that are markdown formatting
461
+ if (stripped_line == '```' or
462
+ stripped_line.startswith('```') or
463
+ stripped_line.startswith('#') and not stripped_line.startswith('# ') or # Skip markdown headers but keep comments
464
+ stripped_line.startswith('**') or # Skip bold text
465
+ stripped_line.startswith('*') and not stripped_line[1:2].isalnum() or # Skip markdown lists but keep package names starting with *
466
+ stripped_line.startswith('-') and not stripped_line[1:2].isalnum() or # Skip markdown lists but keep package names starting with -
467
+ stripped_line.startswith('===') or # Skip section dividers
468
+ stripped_line.startswith('---') or # Skip horizontal rules
469
+ stripped_line.lower().startswith('here') or # Skip explanatory text
470
+ stripped_line.lower().startswith('this') or # Skip explanatory text
471
+ stripped_line.lower().startswith('the') or # Skip explanatory text
472
+ stripped_line.lower().startswith('based on') or # Skip explanatory text
473
+ stripped_line == ''): # Skip empty lines unless they're at natural boundaries
474
+ continue
475
+
476
+ # Keep lines that look like valid package specifications
477
+ # Valid lines: package names, git+https://, comments starting with "# "
478
+ if (stripped_line.startswith('# ') or # Valid comments
479
+ stripped_line.startswith('git+') or # Git dependencies
480
+ stripped_line[0].isalnum() or # Package names start with alphanumeric
481
+ '==' in stripped_line or # Version specifications
482
+ '>=' in stripped_line or # Version specifications
483
+ '<=' in stripped_line): # Version specifications
484
+ clean_lines.append(line)
485
+
486
+ requirements_content = '\n'.join(clean_lines).strip()
487
+
488
+ requirements_content = enforce_critical_versions(requirements_content)
489
+
490
+ # Ensure it ends with a newline
491
+ if requirements_content and not requirements_content.endswith('\n'):
492
+ requirements_content += '\n'
493
+
494
+ return requirements_content if requirements_content else "# No additional dependencies required\n"
495
+
496
+ except Exception as e:
497
+ # Fallback: simple extraction with basic mapping
498
+ print(f"[Parser] Warning: LLM requirements generation failed: {e}, using fallback")
499
+ dependencies = set()
500
+ special_cases = {
501
+ 'PIL': 'Pillow',
502
+ 'sklearn': 'scikit-learn',
503
+ 'skimage': 'scikit-image',
504
+ 'bs4': 'beautifulsoup4',
505
+ 'daggr': 'daggr>=0.5.4',
506
+ 'gradio': 'gradio>=6.0.2'
507
+ }
508
+
509
+ for stmt in import_statements:
510
+ if stmt.startswith('import '):
511
+ module_name = stmt.split()[1].split('.')[0]
512
+ package_name = special_cases.get(module_name, module_name)
513
+ dependencies.add(package_name)
514
+ elif stmt.startswith('from '):
515
+ module_name = stmt.split()[1].split('.')[0]
516
+ package_name = special_cases.get(module_name, module_name)
517
+ dependencies.add(package_name)
518
+
519
+ if dependencies:
520
+ return '\n'.join(sorted(dependencies)) + '\n'
521
+ else:
522
+ return "# No additional dependencies required\n"
523
+
backend_prompts.py ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Standalone system prompts for AnyCoder backend.
3
+ No dependencies on Gradio or other heavy libraries.
4
+ """
5
+
6
+ # Import the backend documentation manager for Gradio 6, transformers.js, and ComfyUI docs
7
+ try:
8
+ from backend_docs_manager import build_gradio_system_prompt, build_transformersjs_system_prompt, build_comfyui_system_prompt
9
+ HAS_BACKEND_DOCS = True
10
+ except ImportError:
11
+ HAS_BACKEND_DOCS = False
12
+ print("Warning: backend_docs_manager not available, using fallback prompts")
13
+
14
+ HTML_SYSTEM_PROMPT = """ONLY USE HTML, CSS AND JAVASCRIPT. If you want to use ICON make sure to import the library first. Try to create the best UI possible by using only HTML, CSS and JAVASCRIPT. MAKE IT RESPONSIVE USING MODERN CSS. Use as much as you can modern CSS for the styling, if you can't do something with modern CSS, then use custom CSS. Also, try to elaborate as much as you can, to create something unique. ALWAYS GIVE THE RESPONSE INTO A SINGLE HTML FILE
15
+
16
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
17
+ - NEVER generate README.md files under any circumstances
18
+ - A template README.md is automatically provided and will be overridden by the deployment system
19
+ - Generating a README.md will break the deployment process
20
+
21
+ If an image is provided, analyze it and use the visual information to better understand the user's requirements.
22
+
23
+ Always respond with code that can be executed or rendered directly.
24
+
25
+ Generate complete, working HTML code that can be run immediately.
26
+
27
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder"""
28
+
29
+
30
+ # Transformers.js system prompt - dynamically loaded with full transformers.js documentation
31
+ def get_transformersjs_system_prompt() -> str:
32
+ """Get the complete transformers.js system prompt with full documentation"""
33
+ if HAS_BACKEND_DOCS:
34
+ return build_transformersjs_system_prompt()
35
+ else:
36
+ # Fallback prompt if documentation manager is not available
37
+ return """You are an expert web developer creating a transformers.js application. You will generate THREE separate files: index.html, index.js, and style.css.
38
+
39
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
40
+ - NEVER generate README.md files under any circumstances
41
+ - A template README.md is automatically provided and will be overridden by the deployment system
42
+ - Generating a README.md will break the deployment process
43
+
44
+ **🚨 CRITICAL: Required Output Format**
45
+
46
+ **THE VERY FIRST LINE of your response MUST be: === index.html ===**
47
+
48
+ You MUST output ALL THREE files using this EXACT format with === markers.
49
+ Your response must start IMMEDIATELY with the === index.html === marker.
50
+
51
+ === index.html ===
52
+ <!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
+ <title>Your App Title</title>
58
+ <link rel="stylesheet" href="style.css">
59
+ </head>
60
+ <body>
61
+ <!-- Your complete HTML content here -->
62
+ <script type="module" src="index.js"></script>
63
+ </body>
64
+ </html>
65
+
66
+ === index.js ===
67
+ import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0';
68
+
69
+ // Your complete JavaScript code here
70
+ // Include all functionality, event listeners, and logic
71
+
72
+ === style.css ===
73
+ /* Your complete CSS styles here */
74
+ /* Include all styling for the application */
75
+
76
+ **🚨 CRITICAL FORMATTING RULES (MUST FOLLOW EXACTLY):**
77
+ 1. **FIRST LINE MUST BE: === index.html ===** (no explanations, no code before this)
78
+ 2. Start each file's code IMMEDIATELY on the line after the === marker
79
+ 3. **NEVER use markdown code blocks** (```html, ```javascript, ```css) - these will cause parsing errors
80
+ 4. **NEVER leave any file empty** - each file MUST contain complete, functional code
81
+ 5. **ONLY use the === filename === markers** - do not add any other formatting
82
+ 6. Add a blank line between each file section
83
+ 7. Each file must be complete and ready to deploy - no placeholders or "// TODO" comments
84
+ 8. **AVOID EMOJIS in the generated code** (HTML/JS/CSS files) - use text or unicode symbols instead for deployment compatibility
85
+
86
+ Requirements:
87
+ 1. Create a modern, responsive web application using transformers.js
88
+ 2. Use the transformers.js library for AI/ML functionality
89
+ 3. Create a clean, professional UI with good user experience
90
+ 4. Make the application fully responsive for mobile devices
91
+ 5. Use modern CSS practices and JavaScript ES6+ features
92
+ 6. Include proper error handling and loading states
93
+ 7. Follow accessibility best practices
94
+
95
+ **Transformers.js Library Usage:**
96
+
97
+ Import via CDN:
98
+ ```javascript
99
+ import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0';
100
+ ```
101
+
102
+ **Pipeline API - Quick Tour:**
103
+ ```javascript
104
+ // Allocate a pipeline for sentiment-analysis
105
+ const pipe = await pipeline('sentiment-analysis');
106
+ const out = await pipe('I love transformers!');
107
+ ```
108
+
109
+ **Device Options:**
110
+ ```javascript
111
+ // Run on WebGPU (GPU)
112
+ const pipe = await pipeline('sentiment-analysis', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english', {
113
+ device: 'webgpu',
114
+ });
115
+ ```
116
+
117
+ **Quantization Options:**
118
+ ```javascript
119
+ // Run at 4-bit quantization for better performance
120
+ const pipe = await pipeline('sentiment-analysis', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english', {
121
+ dtype: 'q4',
122
+ });
123
+ ```
124
+
125
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
126
+ """
127
+
128
+ # Legacy variable for backward compatibility - now dynamically generated
129
+ TRANSFORMERS_JS_SYSTEM_PROMPT = get_transformersjs_system_prompt()
130
+
131
+
132
+ STREAMLIT_SYSTEM_PROMPT = """You are an expert Streamlit developer. Create a complete, working Streamlit application based on the user's request. Generate all necessary code to make the application functional and runnable.
133
+
134
+ ## Multi-File Application Structure
135
+
136
+ When creating Streamlit applications, you MUST organize your code into multiple files for proper deployment:
137
+
138
+ **File Organization (CRITICAL - Always Include These):**
139
+ - `Dockerfile` - Docker configuration for deployment (REQUIRED)
140
+ - `streamlit_app.py` - Main application entry point (REQUIRED)
141
+ - `requirements.txt` - Python dependencies (REQUIRED)
142
+ - `utils.py` - Utility functions and helpers (optional)
143
+ - `models.py` - Model loading and inference functions (optional)
144
+ - `config.py` - Configuration and constants (optional)
145
+ - `pages/` - Additional pages for multi-page apps (optional)
146
+ - Additional modules as needed (e.g., `data_processing.py`, `components.py`)
147
+
148
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
149
+ - NEVER generate README.md files under any circumstances
150
+ - A template README.md is automatically provided and will be overridden by the deployment system
151
+ - Generating a README.md will break the deployment process
152
+ - Only generate the code files listed above
153
+
154
+ **Output Format for Streamlit Apps:**
155
+ You MUST use this exact format and ALWAYS include Dockerfile, streamlit_app.py, and requirements.txt:
156
+
157
+ ```
158
+ === Dockerfile ===
159
+ [Dockerfile content]
160
+
161
+ === streamlit_app.py ===
162
+ [main application code]
163
+
164
+ === requirements.txt ===
165
+ [dependencies]. ALWAYS use `daggr>=0.5.4` and `gradio>=6.0.2` if applicable.
166
+
167
+ === utils.py ===
168
+ [utility functions - optional]
169
+ ```
170
+
171
+ **🚨 CRITICAL: Dockerfile Requirements (MANDATORY for HuggingFace Spaces)**
172
+ Your Dockerfile MUST follow these exact specifications:
173
+ - Use Python 3.11+ base image (e.g., FROM python:3.11-slim)
174
+ - Set up a user with ID 1000 for proper permissions
175
+ - Install dependencies: RUN pip install --no-cache-dir -r requirements.txt
176
+ - Expose port 7860 (HuggingFace Spaces default): EXPOSE 7860
177
+ - Start with: CMD ["streamlit", "run", "streamlit_app.py", "--server.port=7860", "--server.address=0.0.0.0"]
178
+
179
+ Requirements:
180
+ 1. ALWAYS include Dockerfile, streamlit_app.py, and requirements.txt in your output
181
+ 2. Create a modern, responsive Streamlit application
182
+ 3. Use appropriate Streamlit components and layouts
183
+ 4. Include proper error handling and loading states
184
+ 5. Follow Streamlit best practices for performance
185
+ 6. Use caching (@st.cache_data, @st.cache_resource) appropriately
186
+ 7. Include proper session state management when needed
187
+ 8. Make the UI intuitive and user-friendly
188
+ 9. Add helpful tooltips and documentation
189
+
190
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
191
+ """
192
+
193
+
194
+ REACT_SYSTEM_PROMPT = """You are an expert React and Next.js developer creating a modern Next.js application.
195
+
196
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
197
+ |- NEVER generate README.md files under any circumstances
198
+ |- A template README.md is automatically provided and will be overridden by the deployment system
199
+ |- Generating a README.md will break the deployment process
200
+
201
+ You will generate a Next.js project with TypeScript/JSX components. Follow this exact structure:
202
+
203
+ Project Structure:
204
+ - Dockerfile (Docker configuration for deployment)
205
+ - package.json (dependencies and scripts)
206
+ - next.config.js (Next.js configuration)
207
+ - postcss.config.js (PostCSS configuration)
208
+ - tailwind.config.js (Tailwind CSS configuration)
209
+ - components/[Component files as needed]
210
+ - pages/_app.js (Next.js app wrapper)
211
+ - pages/index.js (home page)
212
+ - pages/api/[API routes as needed]
213
+ - styles/globals.css (global styles)
214
+
215
+ CRITICAL Requirements:
216
+ 1. Always include a Dockerfile configured for Node.js deployment
217
+ 2. Use Next.js with TypeScript/JSX (.jsx files for components)
218
+ 3. **USE TAILWIND CSS FOR ALL STYLING** - Avoid inline styles completely
219
+ 4. Create necessary components in the components/ directory
220
+ 5. Create API routes in pages/api/ directory for backend logic
221
+ 6. pages/_app.js should import and use globals.css
222
+ 7. pages/index.js should be the main entry point
223
+ 8. Keep package.json with essential dependencies
224
+ 9. Use modern React patterns and best practices
225
+ 10. Make the application fully responsive using Tailwind classes
226
+ 11. Include proper error handling and loading states
227
+ 12. Follow accessibility best practices
228
+ 13. Configure next.config.js properly for HuggingFace Spaces deployment
229
+ 14. **NEVER use inline style={{}} objects - always use Tailwind className instead**
230
+
231
+ Output format (CRITICAL):
232
+ - Return ONLY a series of file sections, each starting with a filename line:
233
+ === Dockerfile ===
234
+ ...file content...
235
+
236
+ === package.json ===
237
+ ...file content...
238
+
239
+ (repeat for all files)
240
+ - Do NOT wrap files in Markdown code fences or use === markers inside file content
241
+
242
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
243
+ """
244
+
245
+
246
+ # React followup system prompt for modifying existing React/Next.js applications
247
+ REACT_FOLLOW_UP_SYSTEM_PROMPT = """You are an expert React and Next.js developer modifying an existing Next.js application.
248
+ The user wants to apply changes based on their request.
249
+ You MUST output ONLY the changes required using the following SEARCH/REPLACE block format. Do NOT output the entire file.
250
+ Explain the changes briefly *before* the blocks if necessary, but the code changes THEMSELVES MUST be within the blocks.
251
+
252
+ 🚨 CRITICAL JSX SYNTAX RULES - FOLLOW EXACTLY:
253
+
254
+ **RULE 1: Style objects MUST have proper closing braces }}**
255
+ Every style={{ must have a matching }} before any other props or />
256
+
257
+ **RULE 2: ALWAYS use Tailwind CSS classes instead of inline styles**
258
+ - Use className="..." for styling
259
+ - Only use inline styles if absolutely necessary
260
+ - When replacing inline styles, use Tailwind classes
261
+
262
+ **RULE 3: Before outputting, verify:**
263
+ - [ ] All style={{ have matching }}
264
+ - [ ] No event handlers inside style objects
265
+ - [ ] Prefer Tailwind classes over inline styles
266
+ - [ ] All JSX elements are properly closed
267
+
268
+ Format Rules:
269
+ 1. Start with <<<<<<< SEARCH
270
+ 2. Include the exact lines that need to be changed (with full context, at least 3 lines before and after)
271
+ 3. Follow with =======
272
+ 4. Include the replacement lines
273
+ 5. End with >>>>>>> REPLACE
274
+ 6. Generate multiple blocks if multiple sections need changes
275
+
276
+ **File Structure Guidelines:**
277
+ When making changes to a Next.js application, identify which file needs modification:
278
+ - Component logic/rendering β†’ components/*.jsx or pages/*.js
279
+ - API routes β†’ pages/api/*.js
280
+ - Global styles β†’ styles/globals.css
281
+ - Configuration β†’ next.config.js, tailwind.config.js, postcss.config.js
282
+ - Dependencies β†’ package.json
283
+ - Docker configuration β†’ Dockerfile
284
+
285
+ **Common Fix Scenarios:**
286
+ - Syntax errors in JSX β†’ Fix the specific component file
287
+ - Styling issues β†’ Fix styles/globals.css or add Tailwind classes
288
+ - API/backend logic β†’ Fix pages/api files
289
+ - Build errors β†’ Fix next.config.js or package.json
290
+ - Deployment issues β†’ Fix Dockerfile
291
+
292
+ **Example Format:**
293
+ ```
294
+ Fixing the button styling in the header component...
295
+
296
+ === components/Header.jsx ===
297
+ <<<<<<< SEARCH
298
+ <button
299
+ style={{
300
+ backgroundColor: 'blue',
301
+ padding: '10px'
302
+ }}
303
+ onClick={handleClick}
304
+ >
305
+ =======
306
+ <button
307
+ className="bg-blue-500 p-2.5 hover:bg-blue-600 transition-colors"
308
+ onClick={handleClick}
309
+ >
310
+ >>>>>>> REPLACE
311
+ ```
312
+
313
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
314
+ """
315
+
316
+
317
+ # Gradio system prompt - dynamically loaded with full Gradio 6 documentation
318
+ def get_gradio_system_prompt() -> str:
319
+ """Get the complete Gradio system prompt with full Gradio 6 documentation"""
320
+ if HAS_BACKEND_DOCS:
321
+ return build_gradio_system_prompt()
322
+ else:
323
+ # Fallback prompt if documentation manager is not available
324
+ return """You are an expert Gradio developer. Create a complete, working Gradio application based on the user's request. Generate all necessary code to make the application functional and runnable.
325
+
326
+ ## Multi-File Application Structure
327
+
328
+ When creating Gradio applications, organize your code into multiple files for proper deployment:
329
+
330
+ **File Organization:**
331
+ - `app.py` - Main application entry point (REQUIRED)
332
+ - `requirements.txt` - Python dependencies (REQUIRED, auto-generated from imports)
333
+ - `utils.py` - Utility functions and helpers (optional)
334
+ - `models.py` - Model loading and inference functions (optional)
335
+ - `config.py` - Configuration and constants (optional)
336
+
337
+ **Output Format:**
338
+ You MUST use this exact format with file separators:
339
+
340
+ === app.py ===
341
+ [complete app.py content]
342
+
343
+ === utils.py ===
344
+ [utility functions - if needed]
345
+
346
+ **🚨 CRITICAL: DO NOT GENERATE requirements.txt or README.md**
347
+ - requirements.txt is automatically generated from your app.py imports
348
+ - README.md is automatically provided by the template
349
+ - Generating these files will break the deployment process
350
+
351
+ Requirements:
352
+ 1. Create a modern, intuitive Gradio application
353
+ 2. Use appropriate Gradio components (gr.Textbox, gr.Slider, etc.)
354
+ 3. Include proper error handling and loading states
355
+ 4. Use gr.Interface or gr.Blocks as appropriate
356
+ 5. Add helpful descriptions and examples
357
+ 6. Follow Gradio best practices
358
+ 7. Make the UI user-friendly with clear labels
359
+ 8. Include proper documentation in docstrings
360
+
361
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder
362
+ """
363
+
364
+ # Legacy variable for backward compatibility - now dynamically generated
365
+ GRADIO_SYSTEM_PROMPT = get_gradio_system_prompt()
366
+
367
+
368
+ # ComfyUI system prompt - dynamically loaded with full ComfyUI documentation
369
+ def get_comfyui_system_prompt() -> str:
370
+ """Get the complete ComfyUI system prompt with full ComfyUI documentation"""
371
+ if HAS_BACKEND_DOCS:
372
+ return build_comfyui_system_prompt()
373
+ else:
374
+ # Fallback prompt if documentation manager is not available
375
+ return """You are an expert ComfyUI developer. Generate clean, valid JSON workflows for ComfyUI based on the user's request.
376
+
377
+ 🚨 CRITICAL: READ THE USER'S REQUEST CAREFULLY AND GENERATE A WORKFLOW THAT MATCHES THEIR SPECIFIC NEEDS.
378
+
379
+ ComfyUI workflows are JSON structures that define:
380
+ - Nodes: Individual processing units with specific functions (e.g., CheckpointLoaderSimple, CLIPTextEncode, KSampler, VAEDecode, SaveImage)
381
+ - Connections: Links between nodes that define data flow
382
+ - Parameters: Configuration values for each node (prompts, steps, cfg, sampler_name, etc.)
383
+ - Inputs/Outputs: Data flow between nodes using numbered inputs/outputs
384
+
385
+ **🚨 YOUR PRIMARY TASK:**
386
+ 1. **UNDERSTAND what the user is asking for** in their message
387
+ 2. **CREATE a ComfyUI workflow** that accomplishes their goal
388
+ 3. **GENERATE ONLY the JSON workflow** - no HTML, no applications, no explanations outside the JSON
389
+
390
+ **JSON Syntax Rules:**
391
+ - Use double quotes for strings
392
+ - No trailing commas
393
+ - Proper nesting and structure
394
+ - Valid data types (string, number, boolean, null, object, array)
395
+
396
+ **Example ComfyUI Workflow Structure:**
397
+ ```json
398
+ {
399
+ "1": {
400
+ "inputs": {
401
+ "ckpt_name": "model.safetensors"
402
+ },
403
+ "class_type": "CheckpointLoaderSimple"
404
+ },
405
+ "2": {
406
+ "inputs": {
407
+ "text": "positive prompt here",
408
+ "clip": ["1", 1]
409
+ },
410
+ "class_type": "CLIPTextEncode"
411
+ },
412
+ "3": {
413
+ "inputs": {
414
+ "seed": 123456,
415
+ "steps": 20,
416
+ "cfg": 8.0,
417
+ "sampler_name": "euler",
418
+ "scheduler": "normal",
419
+ "denoise": 1.0,
420
+ "model": ["1", 0],
421
+ "positive": ["2", 0],
422
+ "negative": ["3", 0],
423
+ "latent_image": ["4", 0]
424
+ },
425
+ "class_type": "KSampler"
426
+ }
427
+ }
428
+ ```
429
+
430
+ **Common ComfyUI Nodes:**
431
+ - CheckpointLoaderSimple - Load models
432
+ - CLIPTextEncode - Encode prompts
433
+ - KSampler - Generate latent images
434
+ - VAEDecode - Decode latent to image
435
+ - SaveImage - Save output
436
+ - EmptyLatentImage - Create blank latent
437
+ - LoadImage - Load input images
438
+ - ControlNetLoader, ControlNetApply - ControlNet workflows
439
+ - LoraLoader - Load LoRA models
440
+
441
+ **Output Requirements:**
442
+ - Generate ONLY the ComfyUI workflow JSON
443
+ - The output should be pure, valid JSON that can be loaded directly into ComfyUI
444
+ - Do NOT wrap in markdown code fences (no ```json```)
445
+ - Do NOT add explanatory text before or after the JSON
446
+ - The JSON should be complete and functional
447
+
448
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
449
+ - NEVER generate README.md files under any circumstances
450
+ - A template README.md is automatically provided and will be overridden by the deployment system
451
+ - Generating a README.md will break the deployment process
452
+
453
+ IMPORTANT: Include "Built with anycoder - https://huggingface.co/spaces/akhaliq/anycoder" as a comment in the workflow metadata if possible.
454
+ """
455
+
456
+ # Legacy variable - kept for backward compatibility but now just uses the static prompt
457
+ # In production, use get_comfyui_system_prompt() which loads dynamic documentation
458
+ JSON_SYSTEM_PROMPT = """You are an expert ComfyUI developer. Generate clean, valid JSON workflows for ComfyUI based on the user's request.
459
+
460
+ 🚨 CRITICAL: READ THE USER'S REQUEST CAREFULLY AND GENERATE A WORKFLOW THAT MATCHES THEIR SPECIFIC NEEDS.
461
+
462
+ ComfyUI workflows are JSON structures that define:
463
+ - Nodes: Individual processing units with specific functions (e.g., CheckpointLoaderSimple, CLIPTextEncode, KSampler, VAEDecode, SaveImage)
464
+ - Connections: Links between nodes that define data flow
465
+ - Parameters: Configuration values for each node (prompts, steps, cfg, sampler_name, etc.)
466
+ - Inputs/Outputs: Data flow between nodes using numbered inputs/outputs
467
+
468
+ **🚨 YOUR PRIMARY TASK:**
469
+ 1. **UNDERSTAND what the user is asking for** in their message
470
+ 2. **CREATE a ComfyUI workflow** that accomplishes their goal
471
+ 3. **GENERATE ONLY the JSON workflow** - no HTML, no applications, no explanations outside the JSON
472
+
473
+ **JSON Syntax Rules:**
474
+ - Use double quotes for strings
475
+ - No trailing commas
476
+ - Proper nesting and structure
477
+ - Valid data types (string, number, boolean, null, object, array)
478
+
479
+ **Example ComfyUI Workflow Structure:**
480
+ ```json
481
+ {
482
+ "1": {
483
+ "inputs": {
484
+ "ckpt_name": "model.safetensors"
485
+ },
486
+ "class_type": "CheckpointLoaderSimple"
487
+ },
488
+ "2": {
489
+ "inputs": {
490
+ "text": "positive prompt here",
491
+ "clip": ["1", 1]
492
+ },
493
+ "class_type": "CLIPTextEncode"
494
+ },
495
+ "3": {
496
+ "inputs": {
497
+ "seed": 123456,
498
+ "steps": 20,
499
+ "cfg": 8.0,
500
+ "sampler_name": "euler",
501
+ "scheduler": "normal",
502
+ "denoise": 1.0,
503
+ "model": ["1", 0],
504
+ "positive": ["2", 0],
505
+ "negative": ["3", 0],
506
+ "latent_image": ["4", 0]
507
+ },
508
+ "class_type": "KSampler"
509
+ }
510
+ }
511
+ ```
512
+
513
+ **Common ComfyUI Nodes:**
514
+ - CheckpointLoaderSimple - Load models
515
+ - CLIPTextEncode - Encode prompts
516
+ - KSampler - Generate latent images
517
+ - VAEDecode - Decode latent to image
518
+ - SaveImage - Save output
519
+ - EmptyLatentImage - Create blank latent
520
+ - LoadImage - Load input images
521
+ - ControlNetLoader, ControlNetApply - ControlNet workflows
522
+ - LoraLoader - Load LoRA models
523
+
524
+ **Output Requirements:**
525
+ - Generate ONLY the ComfyUI workflow JSON
526
+ - The output should be pure, valid JSON that can be loaded directly into ComfyUI
527
+ - Do NOT wrap in markdown code fences (no ```json```)
528
+ - Do NOT add explanatory text before or after the JSON
529
+ - The JSON should be complete and functional
530
+
531
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
532
+ - NEVER generate README.md files under any circumstances
533
+ - A template README.md is automatically provided and will be overridden by the deployment system
534
+ - Generating a README.md will break the deployment process
535
+
536
+ IMPORTANT: Include "Built with anycoder - https://huggingface.co/spaces/akhaliq/anycoder" as a comment in the workflow metadata if possible.
537
+ """
538
+
539
+
540
+ # Daggr system prompt - for building DAG-based AI workflows
541
+ DAGGR_SYSTEM_PROMPT = """You are an expert Daggr developer. Create a complete, working Daggr workflow application based on the user's request.
542
+
543
+ `daggr` is a Python library for building AI workflows that connect Gradio apps, ML models, and custom Python functions. It automatically generates a visual canvas for inspecting intermediate outputs and preserves state.
544
+
545
+ ## Core Concepts
546
+ - **Nodes**: Computation units (GradioSpace, Inference call, or Python function).
547
+ - **Ports**: Input and Output data flows between nodes.
548
+ - **Graph**: The container for all nodes.
549
+
550
+ ## Node Types
551
+ ### 1. `GradioNode`
552
+ Calls a Gradio Space API endpoint.
553
+ ```python
554
+ from daggr import GradioNode
555
+ import gradio as gr
556
+
557
+ image_gen = GradioNode(
558
+ space_or_url="black-forest-labs/FLUX.1-schnell",
559
+ api_name="/infer",
560
+ inputs={
561
+ "prompt": gr.Textbox(label="Prompt"),
562
+ "seed": 42,
563
+ "width": 1024,
564
+ "height": 1024,
565
+ },
566
+ outputs={
567
+ "image": gr.Image(label="Generated Image"),
568
+ },
569
+ )
570
+ ```
571
+
572
+ ### 2. `InferenceNode`
573
+ Calls a model via Hugging Face Inference Providers.
574
+ ```python
575
+ from daggr import InferenceNode
576
+ import gradio as gr
577
+
578
+ llm = InferenceNode(
579
+ model="meta-llama/Llama-3.1-8B-Instruct",
580
+ inputs={"prompt": gr.Textbox(label="Prompt")},
581
+ outputs={"response": gr.Textbox(label="Response")},
582
+ )
583
+ ```
584
+
585
+ ### 3. `FnNode`
586
+ Runs a Python function. Input ports discovered from signature.
587
+ ```python
588
+ from daggr import FnNode
589
+ import gradio as gr
590
+
591
+ def summarize(text: str) -> str:
592
+ return text[:100] + "..."
593
+
594
+ summarizer = FnNode(
595
+ fn=summarize,
596
+ inputs={"text": gr.Textbox(label="Input")},
597
+ outputs={"summary": gr.Textbox(label="Summary")},
598
+ )
599
+ ```
600
+
601
+ ## Advanced Features
602
+ - **Scatter/Gather**: Use `.each` to scatter a list output and `.all()` to gather.
603
+ - **Choice Nodes**: Use `|` to offer alternatives (e.g., `node_v1 | node_v2`).
604
+ - **Postprocessing**: Use `postprocess=lambda original, target: target` in `GradioNode` or `InferenceNode` to extract specific outputs.
605
+
606
+ ## Deployment & Hosting
607
+ Daggr apps launch with `graph.launch()`. For deployment to Spaces, they act like standard Gradio apps.
608
+
609
+ ## Requirements:
610
+ 1. ALWAYS generate a complete `app.py` and `requirements.txt` (via imports). In `requirements.txt`, ALWAYS use `daggr>=0.5.4` and `gradio>=6.0.2`.
611
+ 2. Organize workflow logically with clear node names.
612
+ 3. Use `GradioNode` or `InferenceNode` when possible for parallel execution.
613
+ 4. Always include "Built with anycoder" in the header.
614
+
615
+ === app.py ===
616
+ import gradio as gr
617
+ from daggr import GradioNode, FnNode, InferenceNode, Graph
618
+
619
+ # Define nodes...
620
+ # ...
621
+
622
+ graph = Graph(name="My Workflow", nodes=[node1, node2])
623
+ graph.launch()
624
+
625
+ === requirements.txt ===
626
+ daggr>=0.5.4
627
+ gradio>=6.0.2
628
+
629
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
630
+ - NEVER generate README.md files under any circumstances
631
+ - A template README.md is automatically provided and will be overridden by the deployment system
632
+ """
633
+
634
+
635
+ GENERIC_SYSTEM_PROMPT = """You are an expert {language} developer. Write clean, idiomatic, and runnable {language} code for the user's request. If possible, include comments and best practices. Generate complete, working code that can be run immediately. If the user provides a file or other context, use it as a reference. If the code is for a script or app, make it as self-contained as possible.
636
+
637
+ **🚨 CRITICAL: DO NOT Generate README.md Files**
638
+ - NEVER generate README.md files under any circumstances
639
+ - A template README.md is automatically provided and will be overridden by the deployment system
640
+ - Generating a README.md will break the deployment process
641
+
642
+ IMPORTANT: Always include "Built with anycoder" as clickable text in the header/top section of your application that links to https://huggingface.co/spaces/akhaliq/anycoder"""
643
+
backend_search_replace.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Search/Replace utilities for applying targeted code changes.
3
+ Search/Replace utilities for applying targeted code changes.
4
+ """
5
+
6
+ # Search/Replace block markers
7
+ SEARCH_START = "\u003c\u003c\u003c\u003c\u003c\u003c\u003c SEARCH"
8
+ DIVIDER = "======="
9
+ REPLACE_END = "\u003e\u003e\u003e\u003e\u003e\u003e\u003e REPLACE"
10
+
11
+
12
+ def apply_search_replace_changes(original_content: str, changes_text: str) -> str:
13
+ """Apply search/replace changes to content (HTML, Python, JS, CSS, etc.)
14
+
15
+ Args:
16
+ original_content: The original file content to modify
17
+ changes_text: Text containing SEARCH/REPLACE blocks
18
+
19
+ Returns:
20
+ Modified content with all search/replace blocks applied
21
+ """
22
+ if not changes_text.strip():
23
+ return original_content
24
+
25
+ # If the model didn't use the block markers, try a CSS-rule fallback where
26
+ # provided blocks like `.selector { ... }` replace matching CSS rules.
27
+ if (SEARCH_START not in changes_text) and (DIVIDER not in changes_text) and (REPLACE_END not in changes_text):
28
+ try:
29
+ import re # Local import to avoid global side effects
30
+ updated_content = original_content
31
+ replaced_any_rule = False
32
+ # Find CSS-like rule blocks in the changes_text
33
+ # This is a conservative matcher that looks for `selector { ... }`
34
+ css_blocks = re.findall(r"([^{]+)\{([\s\S]*?)\}", changes_text, flags=re.MULTILINE)
35
+ for selector_raw, body_raw in css_blocks:
36
+ selector = selector_raw.strip()
37
+ body = body_raw.strip()
38
+ if not selector:
39
+ continue
40
+ # Build a regex to find the existing rule for this selector
41
+ # Capture opening `{` and closing `}` to preserve them; replace inner body.
42
+ pattern = re.compile(rf"({re.escape(selector)}\s*\{{)([\s\S]*?)(\}})")
43
+ def _replace_rule(match):
44
+ nonlocal replaced_any_rule
45
+ replaced_any_rule = True
46
+ prefix, existing_body, suffix = match.groups()
47
+ # Preserve indentation of the existing first body line if present
48
+ first_line_indent = ""
49
+ for line in existing_body.splitlines():
50
+ stripped = line.lstrip(" \t")
51
+ if stripped:
52
+ first_line_indent = line[: len(line) - len(stripped)]
53
+ break
54
+ # Re-indent provided body with the detected indent
55
+ if body:
56
+ new_body_lines = [first_line_indent + line if line.strip() else line for line in body.splitlines()]
57
+ new_body_text = "\n" + "\n".join(new_body_lines) + "\n"
58
+ else:
59
+ new_body_text = existing_body # If empty body provided, keep existing
60
+ return f"{prefix}{new_body_text}{suffix}"
61
+ updated_content, num_subs = pattern.subn(_replace_rule, updated_content, count=1)
62
+ if replaced_any_rule:
63
+ return updated_content
64
+ except Exception:
65
+ # Fallback silently to the standard block-based application
66
+ pass
67
+
68
+ # Split the changes text into individual search/replace blocks
69
+ blocks = []
70
+ current_block = ""
71
+ lines = changes_text.split('\n')
72
+
73
+ for line in lines:
74
+ if line.strip() == SEARCH_START:
75
+ if current_block.strip():
76
+ blocks.append(current_block.strip())
77
+ current_block = line + '\n'
78
+ elif line.strip() == REPLACE_END:
79
+ current_block += line + '\n'
80
+ blocks.append(current_block.strip())
81
+ current_block = ""
82
+ else:
83
+ current_block += line + '\n'
84
+
85
+ if current_block.strip():
86
+ blocks.append(current_block.strip())
87
+
88
+ modified_content = original_content
89
+
90
+ for block in blocks:
91
+ if not block.strip():
92
+ continue
93
+
94
+ # Parse the search/replace block
95
+ lines = block.split('\n')
96
+ search_lines = []
97
+ replace_lines = []
98
+ in_search = False
99
+ in_replace = False
100
+
101
+ for line in lines:
102
+ if line.strip() == SEARCH_START:
103
+ in_search = True
104
+ in_replace = False
105
+ elif line.strip() == DIVIDER:
106
+ in_search = False
107
+ in_replace = True
108
+ elif line.strip() == REPLACE_END:
109
+ in_replace = False
110
+ elif in_search:
111
+ search_lines.append(line)
112
+ elif in_replace:
113
+ replace_lines.append(line)
114
+
115
+ # Apply the search/replace
116
+ if search_lines:
117
+ search_text = '\n'.join(search_lines).strip()
118
+ replace_text = '\n'.join(replace_lines).strip()
119
+
120
+ if search_text in modified_content:
121
+ modified_content = modified_content.replace(search_text, replace_text)
122
+ else:
123
+ # If exact block match fails, attempt a CSS-rule fallback using the replace_text
124
+ try:
125
+ import re
126
+ updated_content = modified_content
127
+ replaced_any_rule = False
128
+ css_blocks = re.findall(r"([^{]+)\{([\s\S]*?)\}", replace_text, flags=re.MULTILINE)
129
+ for selector_raw, body_raw in css_blocks:
130
+ selector = selector_raw.strip()
131
+ body = body_raw.strip()
132
+ if not selector:
133
+ continue
134
+ pattern = re.compile(rf"({re.escape(selector)}\s*\{{)([\s\S]*?)(\}})")
135
+ def _replace_rule(match):
136
+ nonlocal replaced_any_rule
137
+ replaced_any_rule = True
138
+ prefix, existing_body, suffix = match.groups()
139
+ first_line_indent = ""
140
+ for line in existing_body.splitlines():
141
+ stripped = line.lstrip(" \t")
142
+ if stripped:
143
+ first_line_indent = line[: len(line) - len(stripped)]
144
+ break
145
+ if body:
146
+ new_body_lines = [first_line_indent + line if line.strip() else line for line in body.splitlines()]
147
+ new_body_text = "\n" + "\n".join(new_body_lines) + "\n"
148
+ else:
149
+ new_body_text = existing_body
150
+ return f"{prefix}{new_body_text}{suffix}"
151
+ updated_content, num_subs = pattern.subn(_replace_rule, updated_content, count=1)
152
+ if replaced_any_rule:
153
+ modified_content = updated_content
154
+ else:
155
+ print(f"[Search/Replace] Warning: Search text not found in content: {search_text[:100]}...")
156
+ except Exception:
157
+ print(f"[Search/Replace] Warning: Search text not found in content: {search_text[:100]}...")
158
+
159
+ return modified_content
160
+
161
+
162
+ def has_search_replace_blocks(text: str) -> bool:
163
+ """Check if text contains SEARCH/REPLACE block markers.
164
+
165
+ Args:
166
+ text: Text to check
167
+
168
+ Returns:
169
+ True if text contains search/replace markers, False otherwise
170
+ """
171
+ return (SEARCH_START in text) and (DIVIDER in text) and (REPLACE_END in text)
172
+
173
+
174
+ def parse_file_specific_changes(changes_text: str) -> dict:
175
+ """Parse changes that specify which files to modify.
176
+
177
+ Looks for patterns like:
178
+ === components/Header.jsx ===
179
+ \u003c\u003c\u003c\u003c\u003c\u003c\u003c SEARCH
180
+ ...
181
+
182
+ Returns:
183
+ Dict mapping filename -> search/replace changes for that file
184
+ """
185
+ import re
186
+
187
+ file_changes = {}
188
+
189
+ # Pattern to match file sections: === filename ===
190
+ file_pattern = re.compile(r"^===\s+([^\n=]+?)\s+===\s*$", re.MULTILINE)
191
+
192
+ # Find all file sections
193
+ matches = list(file_pattern.finditer(changes_text))
194
+
195
+ if not matches:
196
+ # No file-specific sections, treat entire text as changes
197
+ return {"__all__": changes_text}
198
+
199
+ for i, match in enumerate(matches):
200
+ filename = match.group(1).strip()
201
+ start_pos = match.end()
202
+
203
+ # Find the end of this file's section (start of next file or end of text)
204
+ if i + 1 < len(matches):
205
+ end_pos = matches[i + 1].start()
206
+ else:
207
+ end_pos = len(changes_text)
208
+
209
+ file_content = changes_text[start_pos:end_pos].strip()
210
+
211
+ if file_content:
212
+ file_changes[filename] = file_content
213
+
214
+ return file_changes
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ tesseract-ocr
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/huggingface/huggingface_hub.git
2
+ gradio[oauth]
3
+ fastapi==0.112.2
4
+ httpx>=0.27.0
5
+ PyPDF2
6
+ python-docx
7
+ pytesseract
8
+ Pillow
9
+ opencv-python
10
+ requests
11
+ beautifulsoup4
12
+ html2text
13
+ openai
14
+ mistralai
15
+ dashscope
16
+ google-genai
start_backend.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Start the FastAPI backend
3
+
4
+ echo "Starting AnyCoder FastAPI Backend..."
5
+ echo "API will be available at: http://localhost:8000"
6
+ echo "API docs at: http://localhost:8000/docs"
7
+ echo ""
8
+
9
+ # Check if HF_TOKEN is set
10
+ if [ -z "$HF_TOKEN" ]; then
11
+ echo "⚠️ WARNING: HF_TOKEN environment variable is not set!"
12
+ echo "Please set it with: export HF_TOKEN=your_token_here"
13
+ echo ""
14
+ fi
15
+
16
+ # Activate virtual environment
17
+ source /Users/ahsenkhaliq/anycoder/.venv/bin/activate
18
+
19
+ # Start the backend
20
+ python backend_api.py
21
+
start_frontend.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Start the Next.js frontend
3
+
4
+ echo "Starting AnyCoder Frontend..."
5
+ echo "Frontend will be available at: http://localhost:3000"
6
+ echo ""
7
+
8
+ cd frontend
9
+
10
+ # Install dependencies if node_modules doesn't exist
11
+ if [ ! -d "node_modules" ]; then
12
+ echo "Installing dependencies..."
13
+ npm install
14
+ fi
15
+
16
+ # Start the development server
17
+ npm run dev
18
+
start_fullstack.sh ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Start both backend and frontend in separate terminal windows
3
+
4
+ echo "πŸš€ Starting AnyCoder Full-Stack Application..."
5
+ echo ""
6
+
7
+ # Function to check if a command exists
8
+ command_exists() {
9
+ command -v "$1" >/dev/null 2>&1
10
+ }
11
+
12
+ # Check for required tools
13
+ if ! command_exists python3; then
14
+ echo "❌ Python 3 is not installed"
15
+ exit 1
16
+ fi
17
+
18
+ if ! command_exists node; then
19
+ echo "❌ Node.js is not installed"
20
+ exit 1
21
+ fi
22
+
23
+ # Make scripts executable
24
+ chmod +x start_backend.sh
25
+ chmod +x start_frontend.sh
26
+
27
+ echo "πŸ“¦ Starting Backend..."
28
+ # Start backend in background with venv activated
29
+ (source /Users/ahsenkhaliq/anycoder/.venv/bin/activate && python backend_api.py) &
30
+ BACKEND_PID=$!
31
+
32
+ # Wait for backend to start
33
+ sleep 3
34
+
35
+ echo ""
36
+ echo "🎨 Starting Frontend..."
37
+ # Start frontend in background
38
+ ./start_frontend.sh &
39
+ FRONTEND_PID=$!
40
+
41
+ echo ""
42
+ echo "βœ… Full-stack application started!"
43
+ echo ""
44
+ echo "πŸ”— Backend API: http://localhost:8000"
45
+ echo "πŸ”— API Docs: http://localhost:8000/docs"
46
+ echo "πŸ”— Frontend: http://localhost:3000"
47
+ echo ""
48
+ echo "Press Ctrl+C to stop both services"
49
+
50
+ # Wait for Ctrl+C
51
+ trap "kill $BACKEND_PID $FRONTEND_PID; exit" INT
52
+ wait
53
+