akhaliq HF Staff commited on
Commit
0827363
·
1 Parent(s): 6f51ecd

add redesign feature fix

Browse files
backend_api.py CHANGED
@@ -240,6 +240,20 @@ class ImportResponse(BaseModel):
240
  metadata: Dict
241
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  # Mock authentication for development
244
  # In production, integrate with HuggingFace OAuth
245
  class MockAuth:
@@ -1259,6 +1273,93 @@ async def deploy(
1259
  )
1260
 
1261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1262
  @app.post("/api/import", response_model=ImportResponse)
1263
  async def import_project(request: ImportRequest):
1264
  """
 
240
  metadata: Dict
241
 
242
 
243
+ class PullRequestRequest(BaseModel):
244
+ repo_id: str # username/space-name
245
+ code: str
246
+ language: str
247
+ pr_title: Optional[str] = None
248
+ pr_description: Optional[str] = None
249
+
250
+
251
+ class PullRequestResponse(BaseModel):
252
+ success: bool
253
+ message: str
254
+ pr_url: Optional[str] = None
255
+
256
+
257
  # Mock authentication for development
258
  # In production, integrate with HuggingFace OAuth
259
  class MockAuth:
 
1273
  )
1274
 
1275
 
1276
+ @app.post("/api/create-pr", response_model=PullRequestResponse)
1277
+ async def create_pull_request(
1278
+ request: PullRequestRequest,
1279
+ authorization: Optional[str] = Header(None)
1280
+ ):
1281
+ """Create a Pull Request on an existing HuggingFace Space with redesigned code"""
1282
+ print(f"[PR] ========== NEW PULL REQUEST ==========")
1283
+ print(f"[PR] Repo ID: {request.repo_id}")
1284
+ print(f"[PR] Language: {request.language}")
1285
+ print(f"[PR] PR Title: {request.pr_title}")
1286
+
1287
+ auth = get_auth_from_header(authorization)
1288
+
1289
+ if not auth.is_authenticated():
1290
+ raise HTTPException(status_code=401, detail="Authentication required")
1291
+
1292
+ # Check if this is dev mode
1293
+ if auth.token and auth.token.startswith("dev_token_"):
1294
+ return PullRequestResponse(
1295
+ success=False,
1296
+ message="Dev mode: PR creation not available in dev mode. Please use production authentication.",
1297
+ pr_url=None
1298
+ )
1299
+
1300
+ # Production mode with real OAuth token
1301
+ try:
1302
+ from backend_deploy import create_pull_request_on_space
1303
+
1304
+ user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
1305
+
1306
+ if not user_token:
1307
+ raise HTTPException(status_code=401, detail="No HuggingFace token available. Please sign in first.")
1308
+
1309
+ print(f"[PR] Creating PR with token (first 10 chars): {user_token[:10]}...")
1310
+
1311
+ # Create the pull request
1312
+ success, message, pr_url = create_pull_request_on_space(
1313
+ repo_id=request.repo_id,
1314
+ code=request.code,
1315
+ language=request.language,
1316
+ token=user_token,
1317
+ pr_title=request.pr_title,
1318
+ pr_description=request.pr_description
1319
+ )
1320
+
1321
+ print(f"[PR] Result:")
1322
+ print(f"[PR] - Success: {success}")
1323
+ print(f"[PR] - Message: {message}")
1324
+ print(f"[PR] - PR URL: {pr_url}")
1325
+
1326
+ if success:
1327
+ return PullRequestResponse(
1328
+ success=True,
1329
+ message=message,
1330
+ pr_url=pr_url
1331
+ )
1332
+ else:
1333
+ # Provide user-friendly error messages
1334
+ if "401" in message or "Unauthorized" in message:
1335
+ raise HTTPException(
1336
+ status_code=401,
1337
+ detail="Authentication failed. Please sign in again with HuggingFace."
1338
+ )
1339
+ elif "403" in message or "Forbidden" in message or "Permission" in message:
1340
+ raise HTTPException(
1341
+ status_code=403,
1342
+ detail="Permission denied. You may not have write access to this space."
1343
+ )
1344
+ else:
1345
+ raise HTTPException(
1346
+ status_code=500,
1347
+ detail=message
1348
+ )
1349
+
1350
+ except HTTPException:
1351
+ raise
1352
+ except Exception as e:
1353
+ import traceback
1354
+ error_details = traceback.format_exc()
1355
+ print(f"[PR] Error: {error_details}")
1356
+
1357
+ raise HTTPException(
1358
+ status_code=500,
1359
+ detail=f"Failed to create pull request: {str(e)}"
1360
+ )
1361
+
1362
+
1363
  @app.post("/api/import", response_model=ImportResponse)
1364
  async def import_project(request: ImportRequest):
1365
  """
backend_deploy.py CHANGED
@@ -1133,3 +1133,156 @@ def list_user_spaces(
1133
  except Exception as e:
1134
  return False, f"Failed to list spaces: {str(e)}", None
1135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1133
  except Exception as e:
1134
  return False, f"Failed to list spaces: {str(e)}", None
1135
 
1136
+
1137
+ def create_pull_request_on_space(
1138
+ repo_id: str,
1139
+ code: str,
1140
+ language: str,
1141
+ token: Optional[str] = None,
1142
+ pr_title: Optional[str] = None,
1143
+ pr_description: Optional[str] = None
1144
+ ) -> Tuple[bool, str, Optional[str]]:
1145
+ """
1146
+ Create a Pull Request on an existing HuggingFace Space with redesigned code
1147
+
1148
+ Args:
1149
+ repo_id: Full repo ID (username/space-name)
1150
+ code: New code to propose
1151
+ language: Language/framework type
1152
+ token: HuggingFace API token
1153
+ pr_title: Title for the PR (default: "Redesign from AnyCoder")
1154
+ pr_description: Description for the PR
1155
+
1156
+ Returns:
1157
+ Tuple of (success: bool, message: str, pr_url: Optional[str])
1158
+ """
1159
+ if not token:
1160
+ token = os.getenv("HF_TOKEN")
1161
+ if not token:
1162
+ return False, "No HuggingFace token provided", None
1163
+
1164
+ try:
1165
+ api = HfApi(token=token)
1166
+
1167
+ # Default PR title and description
1168
+ if not pr_title:
1169
+ pr_title = "🎨 Redesign from AnyCoder"
1170
+
1171
+ if not pr_description:
1172
+ pr_description = """This Pull Request contains a redesigned version of the app with:
1173
+
1174
+ - ✨ Modern, mobile-friendly design
1175
+ - 🎯 Minimal, clean components
1176
+ - 📱 Responsive layout
1177
+ - 🚀 Improved user experience
1178
+
1179
+ Generated by [AnyCoder](https://huggingface.co/spaces/akhaliq/anycoder)"""
1180
+
1181
+ # Create temporary directory for files
1182
+ with tempfile.TemporaryDirectory() as temp_dir:
1183
+ temp_path = Path(temp_dir)
1184
+
1185
+ # Parse code based on language
1186
+ if language == "transformers.js":
1187
+ try:
1188
+ files = parse_transformers_js_output(code)
1189
+ print(f"[PR] Parsed transformers.js files: {list(files.keys())}")
1190
+
1191
+ # Write transformers.js files
1192
+ for filename, content in files.items():
1193
+ file_path = temp_path / filename
1194
+ file_path.write_text(content, encoding='utf-8')
1195
+
1196
+ except Exception as e:
1197
+ print(f"[PR] Error parsing transformers.js: {e}")
1198
+ return False, f"Error parsing transformers.js output: {str(e)}", None
1199
+
1200
+ elif language == "html":
1201
+ html_code = parse_html_code(code)
1202
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
1203
+
1204
+ elif language == "comfyui":
1205
+ html_code = prettify_comfyui_json_for_html(code)
1206
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
1207
+
1208
+ elif language in ["gradio", "streamlit", "react"]:
1209
+ files = parse_multi_file_python_output(code)
1210
+
1211
+ # Fallback if no files parsed
1212
+ if not files:
1213
+ print(f"[PR] No file markers found, using entire code as main file")
1214
+ cleaned_code = remove_code_block(code)
1215
+ if language == "streamlit":
1216
+ files["streamlit_app.py"] = cleaned_code
1217
+ elif language == "react":
1218
+ files["app.tsx"] = cleaned_code
1219
+ else:
1220
+ files["app.py"] = cleaned_code
1221
+
1222
+ # Write files (create subdirectories if needed)
1223
+ for filename, content in files.items():
1224
+ file_path = temp_path / filename
1225
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1226
+ file_path.write_text(content, encoding='utf-8')
1227
+
1228
+ # Generate requirements.txt if missing for Python apps
1229
+ if language in ["gradio", "streamlit"] and "requirements.txt" not in files:
1230
+ main_app = files.get('streamlit_app.py') or files.get('app.py', '')
1231
+ if main_app:
1232
+ print(f"[PR] Generating requirements.txt from imports")
1233
+ import_statements = extract_import_statements(main_app)
1234
+ requirements_content = generate_requirements_txt_with_llm(import_statements)
1235
+ (temp_path / "requirements.txt").write_text(requirements_content, encoding='utf-8')
1236
+
1237
+ else:
1238
+ # Default: treat as code file
1239
+ files = parse_multi_file_python_output(code)
1240
+ if not files:
1241
+ cleaned_code = remove_code_block(code)
1242
+ files['app.py'] = cleaned_code
1243
+
1244
+ for filename, content in files.items():
1245
+ file_path = temp_path / filename
1246
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1247
+ file_path.write_text(content, encoding='utf-8')
1248
+
1249
+ # Upload files and create PR using create_commit with create_pr=True
1250
+ try:
1251
+ print(f"[PR] Creating pull request on {repo_id}")
1252
+
1253
+ commit_info = api.upload_folder(
1254
+ folder_path=str(temp_path),
1255
+ repo_id=repo_id,
1256
+ repo_type="space",
1257
+ commit_message=pr_title,
1258
+ commit_description=pr_description,
1259
+ create_pr=True # This creates a PR instead of direct commit
1260
+ )
1261
+
1262
+ # Extract PR URL from commit_info
1263
+ pr_url = commit_info.pr_url if hasattr(commit_info, 'pr_url') else None
1264
+ if not pr_url:
1265
+ # Fallback: construct PR URL from pr_num if available
1266
+ pr_num = commit_info.pr_num if hasattr(commit_info, 'pr_num') else None
1267
+ if pr_num:
1268
+ pr_url = f"https://huggingface.co/spaces/{repo_id}/discussions/{pr_num}"
1269
+ else:
1270
+ pr_url = f"https://huggingface.co/spaces/{repo_id}/discussions"
1271
+
1272
+ success_msg = f"✅ Pull Request created! View at: {pr_url}"
1273
+ print(f"[PR] {success_msg}")
1274
+
1275
+ return True, success_msg, pr_url
1276
+
1277
+ except Exception as e:
1278
+ print(f"[PR] Error creating pull request: {e}")
1279
+ import traceback
1280
+ traceback.print_exc()
1281
+ return False, f"Failed to create pull request: {str(e)}", None
1282
+
1283
+ except Exception as e:
1284
+ print(f"[PR] Top-level exception: {type(e).__name__}: {str(e)}")
1285
+ import traceback
1286
+ traceback.print_exc()
1287
+ return False, f"Pull request error: {str(e)}", None
1288
+
frontend/src/components/LandingPage.tsx CHANGED
@@ -67,6 +67,7 @@ export default function LandingPage({
67
  const [redesignUrl, setRedesignUrl] = useState('');
68
  const [isRedesigning, setIsRedesigning] = useState(false);
69
  const [redesignError, setRedesignError] = useState('');
 
70
 
71
  // Debug effect for dropdown state
72
  useEffect(() => {
@@ -289,22 +290,68 @@ export default function LandingPage({
289
  const result = await apiClient.importProject(redesignUrl);
290
 
291
  if (result.status === 'success') {
292
- // Import the code first, then trigger a redesign
293
- if (onImport && result.code) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  onImport(result.code, result.language || 'html', redesignUrl);
295
- // Automatically trigger a redesign prompt after import
296
  setTimeout(() => {
297
- const redesignPrompt = 'Make the app look better with minimal components needed and mobile friendly and modern design';
298
- onStart(redesignPrompt, result.language || 'html', selectedModel);
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  }, 100);
 
 
 
300
  } else {
301
- // Fallback: trigger code generation with redesign context
302
- const redesignMessage = `Redesign this app from ${redesignUrl}: make the app look better with minimal components needed and mobile friendly and modern design`;
303
- onStart(redesignMessage, result.language || 'html', selectedModel);
304
  }
305
-
306
- setShowRedesignDialog(false);
307
- setRedesignUrl('');
308
  } else {
309
  setRedesignError(result.message || 'Failed to import project for redesign');
310
  }
@@ -634,12 +681,18 @@ export default function LandingPage({
634
  setShowImportDialog(false);
635
  setRedesignError('');
636
  }}
637
- className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 font-medium"
638
  >
639
  <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
640
  <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
641
  </svg>
642
  <span>Redesign</span>
 
 
 
 
 
 
643
  </button>
644
 
645
  {/* Redesign Dialog */}
@@ -656,9 +709,24 @@ export default function LandingPage({
656
  onChange={(e) => setRedesignUrl(e.target.value)}
657
  onKeyPress={(e) => e.key === 'Enter' && handleRedesignProject()}
658
  placeholder="https://huggingface.co/spaces/..."
659
- className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-2"
660
  disabled={isRedesigning}
661
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  {redesignError && (
663
  <p className="text-xs text-red-400 mb-2">{redesignError}</p>
664
  )}
@@ -682,7 +750,9 @@ export default function LandingPage({
682
  </button>
683
  </div>
684
  <p className="text-[10px] text-[#86868b] mt-3">
685
- Import and automatically redesign with modern, mobile-friendly design
 
 
686
  </p>
687
  </div>
688
  </div>
@@ -773,3 +843,4 @@ export default function LandingPage({
773
  </div>
774
  );
775
  }
 
 
67
  const [redesignUrl, setRedesignUrl] = useState('');
68
  const [isRedesigning, setIsRedesigning] = useState(false);
69
  const [redesignError, setRedesignError] = useState('');
70
+ const [createPR, setCreatePR] = useState(false); // Default to normal redesign (not PR)
71
 
72
  // Debug effect for dropdown state
73
  useEffect(() => {
 
290
  const result = await apiClient.importProject(redesignUrl);
291
 
292
  if (result.status === 'success') {
293
+ // Extract repo_id from URL for PR creation
294
+ const spaceMatch = redesignUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
295
+ const repoId = spaceMatch ? spaceMatch[1] : null;
296
+
297
+ if (createPR && repoId && onImport && onStart) {
298
+ // Option 1: Create a PR on the imported space
299
+ // First, import and let AI redesign it
300
+ onImport(result.code, result.language || 'html', redesignUrl);
301
+
302
+ // Send redesign prompt with code context
303
+ setTimeout(async () => {
304
+ const redesignPrompt = `I have existing code in the editor that I imported from ${redesignUrl}. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
305
+
306
+ Current code:
307
+ \`\`\`${result.language || 'html'}
308
+ ${result.code}
309
+ \`\`\`
310
+
311
+ Please redesign this with:
312
+ - Minimal, clean components
313
+ - Mobile-first responsive design
314
+ - Modern UI/UX best practices
315
+ - Better visual hierarchy and spacing`;
316
+
317
+ if (onStart) {
318
+ onStart(redesignPrompt, result.language || 'html', selectedModel);
319
+ }
320
+
321
+ // Show info that PR will be created after code generation
322
+ console.log('[Redesign] Will create PR after code generation completes');
323
+ }, 100);
324
+
325
+ setShowRedesignDialog(false);
326
+ setRedesignUrl('');
327
+
328
+ } else if (onImport && onStart) {
329
+ // Option 2: Normal redesign flow (import and generate new code)
330
  onImport(result.code, result.language || 'html', redesignUrl);
331
+
332
  setTimeout(() => {
333
+ const redesignPrompt = `I have existing code in the editor that I imported from ${redesignUrl}. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
334
+
335
+ Current code:
336
+ \`\`\`${result.language || 'html'}
337
+ ${result.code}
338
+ \`\`\`
339
+
340
+ Please redesign this with:
341
+ - Minimal, clean components
342
+ - Mobile-first responsive design
343
+ - Modern UI/UX best practices
344
+ - Better visual hierarchy and spacing`;
345
+ if (onStart) {
346
+ onStart(redesignPrompt, result.language || 'html', selectedModel);
347
+ }
348
  }, 100);
349
+
350
+ setShowRedesignDialog(false);
351
+ setRedesignUrl('');
352
  } else {
353
+ setRedesignError('Missing required callbacks. Please try again.');
 
 
354
  }
 
 
 
355
  } else {
356
  setRedesignError(result.message || 'Failed to import project for redesign');
357
  }
 
681
  setShowImportDialog(false);
682
  setRedesignError('');
683
  }}
684
+ className="relative px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 font-medium overflow-visible"
685
  >
686
  <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
687
  <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
688
  </svg>
689
  <span>Redesign</span>
690
+ {/* Enhanced NEW Badge with glow effect */}
691
+ <span className="absolute -top-2 -right-2 px-2 py-0.5 bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 text-white text-[9px] font-extrabold rounded-full shadow-lg animate-pulse">
692
+ <span className="relative z-10">NEW</span>
693
+ {/* Glow effect */}
694
+ <span className="absolute inset-0 bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 rounded-full blur-sm opacity-75 animate-pulse"></span>
695
+ </span>
696
  </button>
697
 
698
  {/* Redesign Dialog */}
 
709
  onChange={(e) => setRedesignUrl(e.target.value)}
710
  onKeyPress={(e) => e.key === 'Enter' && handleRedesignProject()}
711
  placeholder="https://huggingface.co/spaces/..."
712
+ className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-3"
713
  disabled={isRedesigning}
714
  />
715
+
716
+ {/* PR Option */}
717
+ <label className="flex items-center gap-2 mb-3 cursor-pointer">
718
+ <input
719
+ type="checkbox"
720
+ checked={createPR}
721
+ onChange={(e) => setCreatePR(e.target.checked)}
722
+ disabled={isRedesigning}
723
+ className="w-4 h-4 rounded bg-[#2d2d30] border-[#424245] text-white focus:ring-white focus:ring-offset-0"
724
+ />
725
+ <span className="text-xs text-[#f5f5f7]">
726
+ Create Pull Request on original space
727
+ </span>
728
+ </label>
729
+
730
  {redesignError && (
731
  <p className="text-xs text-red-400 mb-2">{redesignError}</p>
732
  )}
 
750
  </button>
751
  </div>
752
  <p className="text-[10px] text-[#86868b] mt-3">
753
+ {createPR
754
+ ? 'Creates a Pull Request on the original space with your redesign'
755
+ : 'Import and automatically redesign with modern, mobile-friendly design'}
756
  </p>
757
  </div>
758
  </div>
 
843
  </div>
844
  );
845
  }
846
+
frontend/src/lib/api.ts CHANGED
@@ -491,6 +491,17 @@ class ApiClient {
491
  return response.data;
492
  }
493
 
 
 
 
 
 
 
 
 
 
 
 
494
  logout() {
495
  this.token = null;
496
  }
 
491
  return response.data;
492
  }
493
 
494
+ async createPullRequest(repoId: string, code: string, language: string, prTitle?: string, prDescription?: string): Promise<any> {
495
+ const response = await this.client.post('/api/create-pr', {
496
+ repo_id: repoId,
497
+ code,
498
+ language,
499
+ pr_title: prTitle,
500
+ pr_description: prDescription
501
+ });
502
+ return response.data;
503
+ }
504
+
505
  logout() {
506
  this.token = null;
507
  }