KumarArpit8649 commited on
Commit
3635fbe
·
verified ·
1 Parent(s): 29c6710

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.css +186 -0
  2. requirements.txt (1) +69 -0
  3. server.py (1) +266 -0
app.css ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
2
+
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ line-height: 1.6;
12
+ color: #1e293b;
13
+ background: #f8fafc;
14
+ }
15
+
16
+ /* Custom scrollbar styles */
17
+ ::-webkit-scrollbar {
18
+ width: 8px;
19
+ height: 8px;
20
+ }
21
+
22
+ ::-webkit-scrollbar-track {
23
+ background: #f1f5f9;
24
+ border-radius: 8px;
25
+ }
26
+
27
+ ::-webkit-scrollbar-thumb {
28
+ background: #cbd5e1;
29
+ border-radius: 8px;
30
+ }
31
+
32
+ ::-webkit-scrollbar-thumb:hover {
33
+ background: #94a3b8;
34
+ }
35
+
36
+ /* Loading animation */
37
+ @keyframes spin {
38
+ from { transform: rotate(0deg); }
39
+ to { transform: rotate(360deg); }
40
+ }
41
+
42
+ .animate-spin {
43
+ animation: spin 1s linear infinite;
44
+ }
45
+
46
+ /* Code syntax highlighting in preview */
47
+ pre code {
48
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
49
+ }
50
+
51
+ /* Gradient text utilities */
52
+ .gradient-text {
53
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
54
+ -webkit-background-clip: text;
55
+ -webkit-text-fill-color: transparent;
56
+ background-clip: text;
57
+ }
58
+
59
+ /* Glass morphism effect for cards */
60
+ .glass-card {
61
+ background: rgba(255, 255, 255, 0.8);
62
+ backdrop-filter: blur(12px);
63
+ border: 1px solid rgba(255, 255, 255, 0.2);
64
+ }
65
+
66
+ /* Button hover effects */
67
+ .btn-gradient {
68
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
69
+ transition: all 0.3s ease;
70
+ }
71
+
72
+ .btn-gradient:hover {
73
+ background: linear-gradient(135deg, #2563eb, #7c3aed);
74
+ transform: translateY(-1px);
75
+ box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3);
76
+ }
77
+
78
+ /* Iframe responsive wrapper */
79
+ .iframe-wrapper {
80
+ position: relative;
81
+ width: 100%;
82
+ height: 100%;
83
+ overflow: hidden;
84
+ border-radius: 8px;
85
+ }
86
+
87
+ /* Status indicators */
88
+ .status-dot {
89
+ width: 8px;
90
+ height: 8px;
91
+ border-radius: 50%;
92
+ display: inline-block;
93
+ margin-right: 8px;
94
+ }
95
+
96
+ .status-generating {
97
+ background: #f59e0b;
98
+ animation: pulse 2s infinite;
99
+ }
100
+
101
+ .status-complete {
102
+ background: #10b981;
103
+ }
104
+
105
+ .status-error {
106
+ background: #ef4444;
107
+ }
108
+
109
+ @keyframes pulse {
110
+ 0%, 100% { opacity: 1; }
111
+ 50% { opacity: 0.5; }
112
+ }
113
+
114
+ /* File type badges */
115
+ .file-badge {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 4px;
119
+ padding: 4px 8px;
120
+ background: #e2e8f0;
121
+ color: #475569;
122
+ border-radius: 6px;
123
+ font-size: 12px;
124
+ font-weight: 500;
125
+ }
126
+
127
+ .file-badge.html { background: #fef3c7; color: #92400e; }
128
+ .file-badge.jsx { background: #dbeafe; color: #1e40af; }
129
+ .file-badge.css { background: #dcfce7; color: #166534; }
130
+ .file-badge.js { background: #fef3c7; color: #92400e; }
131
+
132
+ /* Preview loading state */
133
+ .preview-loading {
134
+ display: flex;
135
+ flex-direction: column;
136
+ align-items: center;
137
+ justify-content: center;
138
+ height: 100%;
139
+ color: #64748b;
140
+ }
141
+
142
+ /* Responsive design */
143
+ @media (max-width: 768px) {
144
+ .container {
145
+ padding: 16px;
146
+ }
147
+
148
+ .grid.lg\\:grid-cols-2 {
149
+ grid-template-columns: 1fr;
150
+ }
151
+ }
152
+
153
+ /* Toast styling (works with sonner) */
154
+ .toast-container {
155
+ font-family: 'Inter', sans-serif;
156
+ }
157
+
158
+ /* Code block styling */
159
+ .code-block {
160
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
161
+ font-size: 14px;
162
+ line-height: 1.5;
163
+ background: #1e293b;
164
+ color: #e2e8f0;
165
+ padding: 16px;
166
+ border-radius: 8px;
167
+ overflow-x: auto;
168
+ }
169
+
170
+ /* Smooth transitions for all interactive elements */
171
+ button, .btn, input, textarea, select {
172
+ transition: all 0.2s ease;
173
+ }
174
+
175
+ /* Focus states */
176
+ input:focus, textarea:focus, select:focus {
177
+ outline: none;
178
+ ring: 2px solid #3b82f6;
179
+ border-color: #3b82f6;
180
+ }
181
+
182
+ /* Custom selection */
183
+ ::selection {
184
+ background: #3b82f6;
185
+ color: white;
186
+ }
requirements.txt (1) ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.10.0
3
+ black==25.1.0
4
+ boto3==1.40.30
5
+ botocore==1.40.30
6
+ certifi==2025.8.3
7
+ cffi==2.0.0
8
+ charset-normalizer==3.4.3
9
+ click==8.2.1
10
+ cryptography==45.0.7
11
+ dnspython==2.8.0
12
+ ecdsa==0.19.1
13
+ email-validator==2.3.0
14
+ fastapi==0.110.1
15
+ flake8==7.3.0
16
+ h11==0.16.0
17
+ httpcore==1.0.9
18
+ httpx==0.28.1
19
+ idna==3.10
20
+ iniconfig==2.1.0
21
+ isort==6.0.1
22
+ jmespath==1.0.1
23
+ jq==1.10.0
24
+ markdown-it-py==4.0.0
25
+ mccabe==0.7.0
26
+ mdurl==0.1.2
27
+ motor==3.3.1
28
+ mypy==1.18.1
29
+ mypy_extensions==1.1.0
30
+ numpy==2.3.3
31
+ oauthlib==3.3.1
32
+ packaging==25.0
33
+ pandas==2.3.2
34
+ passlib==1.7.4
35
+ pathspec==0.12.1
36
+ platformdirs==4.4.0
37
+ pluggy==1.6.0
38
+ pyasn1==0.6.1
39
+ pycodestyle==2.14.0
40
+ pycparser==2.23
41
+ pydantic==2.11.7
42
+ pydantic_core==2.33.2
43
+ pyflakes==3.4.0
44
+ Pygments==2.19.2
45
+ PyJWT==2.10.1
46
+ pymongo==4.5.0
47
+ pytest==8.4.2
48
+ python-dateutil==2.9.0.post0
49
+ python-dotenv==1.1.1
50
+ python-jose==3.5.0
51
+ python-multipart==0.0.20
52
+ pytz==2025.2
53
+ requests==2.32.5
54
+ requests-oauthlib==2.0.0
55
+ rich==14.1.0
56
+ rsa==4.9.1
57
+ s3transfer==0.14.0
58
+ s5cmd==0.2.0
59
+ shellingham==1.5.4
60
+ six==1.17.0
61
+ sniffio==1.3.1
62
+ starlette==0.37.2
63
+ typer==0.17.4
64
+ typing-inspection==0.4.1
65
+ typing_extensions==4.15.0
66
+ tzdata==2025.2
67
+ urllib3==2.5.0
68
+ uvicorn==0.25.0
69
+ watchfiles==1.1.0
server.py (1) ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import asyncio
5
+ import zipfile
6
+ import io
7
+ from typing import Dict, Any, List, Optional
8
+ from dotenv import load_dotenv
9
+
10
+ from fastapi import FastAPI, HTTPException, Request
11
+ from fastapi.responses import StreamingResponse
12
+ from fastapi.staticfiles import StaticFiles
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel
15
+ import httpx
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Configuration
21
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
22
+ SYSTEM_PROMPT = os.getenv("SYSTEM_PROMPT",
23
+ "You are a helpful coding assistant that produces clean, well-structured, "
24
+ "production-ready code. Generate readable, commented code and follow best practices. "
25
+ "When returning multiple files, enclose each file in a fenced code block with an optional "
26
+ "filename attribute (e.g. ```html filename=index.html). If no filename is provided, default to index.html."
27
+ )
28
+
29
+ # Preflight check for API key
30
+ if not OPENROUTER_API_KEY:
31
+ raise RuntimeError(
32
+ "OPENROUTER_API_KEY is not set. Set it as an environment variable before running. "
33
+ "Get your API key from https://openrouter.ai/keys"
34
+ )
35
+
36
+ app = FastAPI(title="AI Coding Playground", version="1.0.0")
37
+
38
+ # CORS Configuration
39
+ cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
40
+ app.add_middleware(
41
+ CORSMiddleware,
42
+ allow_origins=cors_origins,
43
+ allow_credentials=True,
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+
48
+ # Code parsing regex - tolerant pattern for fenced code blocks
49
+ fence_pattern = re.compile(
50
+ r"```(?:\s*(?P<lang>[\w+-]+))?(?:\s+filename=(?P<fname>(?:\"[^\"]+\"|'[^']+'|\S+)))?\s*\n(?P<body>[\s\S]*?)```",
51
+ re.MULTILINE
52
+ )
53
+
54
+ # Language to file extension mapping
55
+ LANG_TO_EXT = {
56
+ 'html': '.html',
57
+ 'css': '.css',
58
+ 'javascript': '.js',
59
+ 'js': '.js',
60
+ 'typescript': '.ts',
61
+ 'ts': '.ts',
62
+ 'jsx': '.jsx',
63
+ 'tsx': '.tsx',
64
+ 'react': '.jsx',
65
+ 'json': '.json',
66
+ 'python': '.py',
67
+ 'py': '.py',
68
+ }
69
+
70
+ class GenerateRequest(BaseModel):
71
+ prompt: str
72
+
73
+ class ParsedFile(BaseModel):
74
+ filename: str
75
+ content: str
76
+ language: str
77
+
78
+ def parse_code_blocks(text: str) -> Dict[str, str]:
79
+ """Parse fenced code blocks from LLM output with improved React support"""
80
+ files = {}
81
+ matches = fence_pattern.finditer(text)
82
+
83
+ for match in matches:
84
+ lang = match.group('lang') or 'html'
85
+ fname = match.group('fname')
86
+ body = match.group('body').strip()
87
+
88
+ # Clean filename if quoted
89
+ if fname:
90
+ fname = fname.strip('"\'')
91
+ else:
92
+ # Smart default filename based on language
93
+ if lang.lower() in ['jsx', 'tsx', 'react']:
94
+ # Look for component name in the code
95
+ component_match = re.search(r'(?:function|const)\s+([A-Z][a-zA-Z]*)', body)
96
+ if component_match:
97
+ component_name = component_match.group(1)
98
+ fname = f'{component_name}.jsx'
99
+ else:
100
+ fname = 'App.jsx'
101
+ elif lang.lower() == 'javascript' or lang.lower() == 'js':
102
+ # Check if this is actually React code (contains JSX)
103
+ if 'return (' in body and ('<' in body and '>' in body):
104
+ # This looks like JSX, use .jsx extension
105
+ component_match = re.search(r'(?:function|const)\s+([A-Z][a-zA-Z]*)', body)
106
+ if component_match:
107
+ component_name = component_match.group(1)
108
+ fname = f'{component_name}.jsx'
109
+ else:
110
+ fname = 'App.jsx'
111
+ else:
112
+ fname = 'script.js'
113
+ elif lang.lower() == 'css':
114
+ # Try to match CSS filename with JSX files
115
+ if 'App' in body or not files:
116
+ fname = 'App.css'
117
+ else:
118
+ fname = 'styles.css'
119
+ else:
120
+ ext = LANG_TO_EXT.get(lang.lower(), '.html')
121
+ fname = f'index{ext}'
122
+
123
+ files[fname] = body
124
+
125
+ # If no code blocks found but there's content, treat as HTML
126
+ if not files and text:
127
+ files['index.html'] = text
128
+
129
+ return files
130
+
131
+ def create_zip_buffer(files: Dict[str, str]) -> io.BytesIO:
132
+ """Create a zip file buffer from parsed files"""
133
+ zip_buffer = io.BytesIO()
134
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
135
+ for filename, content in files.items():
136
+ zip_file.writestr(filename, content)
137
+ zip_buffer.seek(0)
138
+ return zip_buffer
139
+
140
+ async def stream_openrouter_response(prompt: str):
141
+ """Stream response from OpenRouter API"""
142
+ url = "https://openrouter.ai/api/v1/chat/completions"
143
+ headers = {
144
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
145
+ "Content-Type": "application/json",
146
+ }
147
+
148
+ payload = {
149
+ "model": "qwen/qwen3-coder:free",
150
+ "messages": [
151
+ {"role": "system", "content": SYSTEM_PROMPT},
152
+ {"role": "user", "content": prompt}
153
+ ],
154
+ "stream": True,
155
+ "temperature": 0.7
156
+ }
157
+
158
+ try:
159
+ async with httpx.AsyncClient(timeout=60.0) as client:
160
+ async with client.stream("POST", url, json=payload, headers=headers) as response:
161
+ if response.status_code != 200:
162
+ error_text = await response.aread()
163
+ error_data = {"error": f"OpenRouter API Error: {response.status_code}", "details": error_text.decode()}
164
+ yield f"data: {json.dumps(error_data)}\n\n"
165
+ return
166
+
167
+ full_content = ""
168
+ async for chunk in response.aiter_lines():
169
+ if chunk.startswith("data: "):
170
+ data = chunk[6:] # Remove "data: " prefix
171
+ if data == "[DONE]":
172
+ # Send final parsed files
173
+ parsed_files = parse_code_blocks(full_content)
174
+ final_data = {
175
+ "type": "complete",
176
+ "files": parsed_files,
177
+ "full_content": full_content
178
+ }
179
+ yield f"data: {json.dumps(final_data)}\n\n"
180
+ break
181
+
182
+ try:
183
+ chunk_data = json.loads(data)
184
+ if "choices" in chunk_data and chunk_data["choices"]:
185
+ delta = chunk_data["choices"][0].get("delta", {})
186
+ if "content" in delta:
187
+ content = delta["content"]
188
+ full_content += content
189
+
190
+ # Send streaming content
191
+ stream_data = {
192
+ "type": "content",
193
+ "content": content,
194
+ "full_content": full_content
195
+ }
196
+ yield f"data: {json.dumps(stream_data)}\n\n"
197
+ except json.JSONDecodeError:
198
+ continue
199
+
200
+ except httpx.TimeoutException:
201
+ error_data = {"error": "Request timeout", "details": "The API request timed out. Please try again."}
202
+ yield f"data: {json.dumps(error_data)}\n\n"
203
+ except httpx.RequestError as e:
204
+ error_data = {"error": "Connection error", "details": f"Failed to connect to OpenRouter API: {str(e)}"}
205
+ yield f"data: {json.dumps(error_data)}\n\n"
206
+ except Exception as e:
207
+ error_data = {"error": "Unexpected error", "details": str(e)}
208
+ yield f"data: {json.dumps(error_data)}\n\n"
209
+
210
+ @app.get("/api/health")
211
+ async def health_check():
212
+ """Health check endpoint"""
213
+ return {
214
+ "status": "healthy",
215
+ "api_key_configured": bool(OPENROUTER_API_KEY),
216
+ "system_prompt_configured": bool(SYSTEM_PROMPT)
217
+ }
218
+
219
+ @app.post("/api/generate")
220
+ async def generate_code(request: GenerateRequest):
221
+ """Generate code using OpenRouter API with streaming"""
222
+ if not request.prompt.strip():
223
+ raise HTTPException(status_code=400, detail="Prompt cannot be empty")
224
+
225
+ return StreamingResponse(
226
+ stream_openrouter_response(request.prompt),
227
+ media_type="text/plain",
228
+ headers={
229
+ "Cache-Control": "no-cache",
230
+ "Connection": "keep-alive",
231
+ "Access-Control-Allow-Origin": "*",
232
+ }
233
+ )
234
+
235
+ @app.post("/api/download")
236
+ async def download_files(files: Dict[str, str]):
237
+ """Create and download ZIP file from parsed files"""
238
+ if not files:
239
+ raise HTTPException(status_code=400, detail="No files provided")
240
+
241
+ try:
242
+ zip_buffer = create_zip_buffer(files)
243
+
244
+ return StreamingResponse(
245
+ io.BytesIO(zip_buffer.read()),
246
+ media_type="application/zip",
247
+ headers={
248
+ "Content-Disposition": "attachment; filename=generated-code.zip"
249
+ }
250
+ )
251
+ except Exception as e:
252
+ raise HTTPException(status_code=500, detail=f"Failed to create ZIP file: {str(e)}")
253
+
254
+ # Serve React static files (for Hugging Face Spaces deployment)
255
+ try:
256
+ app.mount("/", StaticFiles(directory="/app/frontend/build", html=True), name="frontend")
257
+ except Exception:
258
+ # For development - React dev server handles frontend
259
+ @app.get("/")
260
+ async def root():
261
+ return {"message": "AI Coding Playground API", "frontend": "Run React dev server separately"}
262
+
263
+ if __name__ == "__main__":
264
+ import uvicorn
265
+ port = int(os.getenv("PORT", 8001))
266
+ uvicorn.run(app, host="0.0.0.0", port=port)