akhaliq HF Staff commited on
Commit
2b0cf6a
Β·
1 Parent(s): 0827363
backend_api.py CHANGED
@@ -254,6 +254,19 @@ class PullRequestResponse(BaseModel):
254
  pr_url: Optional[str] = None
255
 
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  # Mock authentication for development
258
  # In production, integrate with HuggingFace OAuth
259
  class MockAuth:
@@ -1360,6 +1373,101 @@ async def create_pull_request(
1360
  )
1361
 
1362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1363
  @app.post("/api/import", response_model=ImportResponse)
1364
  async def import_project(request: ImportRequest):
1365
  """
 
254
  pr_url: Optional[str] = None
255
 
256
 
257
+ class DuplicateSpaceRequest(BaseModel):
258
+ from_space_id: str # username/space-name
259
+ to_space_name: Optional[str] = None # Just the name, not full ID
260
+ private: bool = False
261
+
262
+
263
+ class DuplicateSpaceResponse(BaseModel):
264
+ success: bool
265
+ message: str
266
+ space_url: Optional[str] = None
267
+ space_id: Optional[str] = None
268
+
269
+
270
  # Mock authentication for development
271
  # In production, integrate with HuggingFace OAuth
272
  class MockAuth:
 
1373
  )
1374
 
1375
 
1376
+ @app.post("/api/duplicate-space", response_model=DuplicateSpaceResponse)
1377
+ async def duplicate_space_endpoint(
1378
+ request: DuplicateSpaceRequest,
1379
+ authorization: Optional[str] = Header(None)
1380
+ ):
1381
+ """Duplicate a HuggingFace Space to the user's account"""
1382
+ print(f"[Duplicate] ========== DUPLICATE SPACE REQUEST ==========")
1383
+ print(f"[Duplicate] From: {request.from_space_id}")
1384
+ print(f"[Duplicate] To: {request.to_space_name or 'auto'}")
1385
+ print(f"[Duplicate] Private: {request.private}")
1386
+
1387
+ auth = get_auth_from_header(authorization)
1388
+
1389
+ if not auth.is_authenticated():
1390
+ raise HTTPException(status_code=401, detail="Authentication required")
1391
+
1392
+ # Check if this is dev mode
1393
+ if auth.token and auth.token.startswith("dev_token_"):
1394
+ return DuplicateSpaceResponse(
1395
+ success=False,
1396
+ message="Dev mode: Space duplication not available in dev mode. Please use production authentication.",
1397
+ space_url=None,
1398
+ space_id=None
1399
+ )
1400
+
1401
+ # Production mode with real OAuth token
1402
+ try:
1403
+ from backend_deploy import duplicate_space_to_user
1404
+
1405
+ user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
1406
+
1407
+ if not user_token:
1408
+ raise HTTPException(status_code=401, detail="No HuggingFace token available. Please sign in first.")
1409
+
1410
+ print(f"[Duplicate] Duplicating space with token (first 10 chars): {user_token[:10]}...")
1411
+
1412
+ # Duplicate the space
1413
+ success, message, space_url = duplicate_space_to_user(
1414
+ from_space_id=request.from_space_id,
1415
+ to_space_name=request.to_space_name,
1416
+ token=user_token,
1417
+ private=request.private
1418
+ )
1419
+
1420
+ print(f"[Duplicate] Result:")
1421
+ print(f"[Duplicate] - Success: {success}")
1422
+ print(f"[Duplicate] - Message: {message}")
1423
+ print(f"[Duplicate] - Space URL: {space_url}")
1424
+
1425
+ if success:
1426
+ # Extract space_id from URL
1427
+ space_id = space_url.split("/spaces/")[-1] if space_url else None
1428
+
1429
+ return DuplicateSpaceResponse(
1430
+ success=True,
1431
+ message=message,
1432
+ space_url=space_url,
1433
+ space_id=space_id
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 access to this space."
1446
+ )
1447
+ elif "404" in message or "not found" in message.lower():
1448
+ raise HTTPException(
1449
+ status_code=404,
1450
+ detail="Space not found. Please check the URL and try again."
1451
+ )
1452
+ else:
1453
+ raise HTTPException(
1454
+ status_code=500,
1455
+ detail=message
1456
+ )
1457
+
1458
+ except HTTPException:
1459
+ raise
1460
+ except Exception as e:
1461
+ import traceback
1462
+ error_details = traceback.format_exc()
1463
+ print(f"[Duplicate] Error: {error_details}")
1464
+
1465
+ raise HTTPException(
1466
+ status_code=500,
1467
+ detail=f"Failed to duplicate space: {str(e)}"
1468
+ )
1469
+
1470
+
1471
  @app.post("/api/import", response_model=ImportResponse)
1472
  async def import_project(request: ImportRequest):
1473
  """
backend_deploy.py CHANGED
@@ -1134,6 +1134,76 @@ def list_user_spaces(
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,
 
1134
  return False, f"Failed to list spaces: {str(e)}", None
1135
 
1136
 
1137
+ def duplicate_space_to_user(
1138
+ from_space_id: str,
1139
+ to_space_name: Optional[str] = None,
1140
+ token: Optional[str] = None,
1141
+ private: bool = False
1142
+ ) -> Tuple[bool, str, Optional[str]]:
1143
+ """
1144
+ Duplicate a HuggingFace Space to the user's account
1145
+
1146
+ Args:
1147
+ from_space_id: Source space ID (username/space-name)
1148
+ to_space_name: Destination space name (just the name, not full ID)
1149
+ token: HuggingFace API token
1150
+ private: Whether the duplicated space should be private
1151
+
1152
+ Returns:
1153
+ Tuple of (success: bool, message: str, space_url: Optional[str])
1154
+ """
1155
+ if not token:
1156
+ token = os.getenv("HF_TOKEN")
1157
+ if not token:
1158
+ return False, "No HuggingFace token provided", None
1159
+
1160
+ try:
1161
+ from huggingface_hub import duplicate_space
1162
+
1163
+ # Get username from token
1164
+ api = HfApi(token=token)
1165
+ user_info = api.whoami()
1166
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
1167
+
1168
+ # If no destination name provided, use original name
1169
+ if not to_space_name:
1170
+ # Extract original space name
1171
+ original_name = from_space_id.split('/')[-1]
1172
+ to_space_name = original_name
1173
+
1174
+ # Clean space name
1175
+ to_space_name = re.sub(r'[^a-z0-9-]', '-', to_space_name.lower())
1176
+ to_space_name = re.sub(r'-+', '-', to_space_name).strip('-')
1177
+
1178
+ # Construct full destination ID
1179
+ to_space_id = f"{username}/{to_space_name}"
1180
+
1181
+ print(f"[Duplicate] Duplicating {from_space_id} to {to_space_id}")
1182
+
1183
+ # Duplicate the space
1184
+ duplicated_repo = duplicate_space(
1185
+ from_id=from_space_id,
1186
+ to_id=to_space_name, # Just the name, not full ID
1187
+ token=token,
1188
+ private=private,
1189
+ exist_ok=True
1190
+ )
1191
+
1192
+ # Extract space URL
1193
+ space_url = f"https://huggingface.co/spaces/{to_space_id}"
1194
+
1195
+ success_msg = f"βœ… Space duplicated! View at: {space_url}"
1196
+ print(f"[Duplicate] {success_msg}")
1197
+
1198
+ return True, success_msg, space_url
1199
+
1200
+ except Exception as e:
1201
+ print(f"[Duplicate] Error: {type(e).__name__}: {str(e)}")
1202
+ import traceback
1203
+ traceback.print_exc()
1204
+ return False, f"Failed to duplicate space: {str(e)}", None
1205
+
1206
+
1207
  def create_pull_request_on_space(
1208
  repo_id: str,
1209
  code: str,
frontend/src/components/LandingPage.tsx CHANGED
@@ -246,23 +246,48 @@ export default function LandingPage({
246
  setImportError('');
247
 
248
  try {
249
- const result = await apiClient.importProject(importUrl);
 
250
 
251
- if (result.status === 'success') {
252
- // Use onImport if available (better UX - directly loads code)
253
- // Otherwise fall back to onStart (sends message to generate)
254
- if (onImport && result.code) {
255
- onImport(result.code, result.language || 'html', importUrl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  } else {
257
- // Fallback: trigger code generation with import context
258
- const importMessage = `Imported from ${importUrl}`;
259
- onStart(importMessage, result.language || 'html', selectedModel);
260
  }
261
-
262
- setShowImportDialog(false);
263
- setImportUrl('');
264
  } else {
265
- setImportError(result.message || 'Failed to import project');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
  } catch (error: any) {
268
  console.error('Import error:', error);
@@ -287,16 +312,29 @@ export default function LandingPage({
287
  setRedesignError('');
288
 
289
  try {
 
 
 
 
 
 
 
 
 
 
 
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
@@ -318,19 +356,30 @@ Please redesign this with:
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'}
@@ -342,22 +391,22 @@ Please redesign this with:
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
  }
358
  } catch (error: any) {
359
  console.error('Redesign error:', error);
360
- setRedesignError(error.response?.data?.message || error.message || 'Failed to import project for redesign');
361
  } finally {
362
  setIsRedesigning(false);
363
  }
 
246
  setImportError('');
247
 
248
  try {
249
+ // Extract space ID from URL for duplication
250
+ const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
251
 
252
+ if (spaceMatch) {
253
+ // This is a HuggingFace Space - duplicate it
254
+ const fromSpaceId = spaceMatch[1];
255
+ console.log('[Import] Duplicating space:', fromSpaceId);
256
+
257
+ const duplicateResult = await apiClient.duplicateSpace(fromSpaceId);
258
+
259
+ if (duplicateResult.success) {
260
+ // Show success message with link to duplicated space
261
+ alert(`βœ… Space duplicated successfully!\n\nView your space: ${duplicateResult.space_url}`);
262
+
263
+ // Also load the code in the editor
264
+ const importResult = await apiClient.importProject(importUrl);
265
+ if (importResult.status === 'success' && onImport && importResult.code) {
266
+ onImport(importResult.code, importResult.language || 'html', duplicateResult.space_url);
267
+ }
268
+
269
+ setShowImportDialog(false);
270
+ setImportUrl('');
271
  } else {
272
+ setImportError(duplicateResult.message || 'Failed to duplicate space');
 
 
273
  }
 
 
 
274
  } else {
275
+ // Not a Space URL - fall back to regular import
276
+ const result = await apiClient.importProject(importUrl);
277
+
278
+ if (result.status === 'success') {
279
+ if (onImport && result.code) {
280
+ onImport(result.code, result.language || 'html', importUrl);
281
+ } else {
282
+ const importMessage = `Imported from ${importUrl}`;
283
+ onStart(importMessage, result.language || 'html', selectedModel);
284
+ }
285
+
286
+ setShowImportDialog(false);
287
+ setImportUrl('');
288
+ } else {
289
+ setImportError(result.message || 'Failed to import project');
290
+ }
291
  }
292
  } catch (error: any) {
293
  console.error('Import error:', error);
 
312
  setRedesignError('');
313
 
314
  try {
315
+ // Extract space ID from URL
316
+ const spaceMatch = redesignUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
317
+ const repoId = spaceMatch ? spaceMatch[1] : null;
318
+
319
+ if (!repoId) {
320
+ setRedesignError('Please enter a valid HuggingFace Space URL');
321
+ setIsRedesigning(false);
322
+ return;
323
+ }
324
+
325
+ // Import the code first
326
  const result = await apiClient.importProject(redesignUrl);
327
 
328
+ if (result.status !== 'success') {
329
+ setRedesignError(result.message || 'Failed to import project for redesign');
330
+ setIsRedesigning(false);
331
+ return;
332
+ }
333
+
334
+ if (createPR) {
335
+ // Option 1: Create a PR on the original space
336
+ // Import code and let AI redesign it
337
+ if (onImport && onStart) {
338
  onImport(result.code, result.language || 'html', redesignUrl);
339
 
340
  // Send redesign prompt with code context
 
356
  onStart(redesignPrompt, result.language || 'html', selectedModel);
357
  }
358
 
 
359
  console.log('[Redesign] Will create PR after code generation completes');
360
  }, 100);
361
 
362
  setShowRedesignDialog(false);
363
  setRedesignUrl('');
364
+ } else {
365
+ setRedesignError('Missing required callbacks. Please try again.');
366
+ }
367
+ } else {
368
+ // Option 2: Duplicate the space and then apply redesign
369
+ console.log('[Redesign] Duplicating space for redesign:', repoId);
370
+
371
+ const duplicateResult = await apiClient.duplicateSpace(repoId);
372
+
373
+ if (duplicateResult.success) {
374
+ // Show success message
375
+ alert(`βœ… Space duplicated successfully!\n\nYour space: ${duplicateResult.space_url}\n\nNow generating redesign...`);
376
 
377
+ // Load the code and trigger redesign
378
+ if (onImport && onStart) {
379
+ onImport(result.code, result.language || 'html', duplicateResult.space_url);
380
+
381
+ setTimeout(() => {
382
+ const redesignPrompt = `I have existing code in the editor that I duplicated from ${redesignUrl}. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
383
 
384
  Current code:
385
  \`\`\`${result.language || 'html'}
 
391
  - Mobile-first responsive design
392
  - Modern UI/UX best practices
393
  - Better visual hierarchy and spacing`;
394
+
395
+ if (onStart) {
396
+ onStart(redesignPrompt, result.language || 'html', selectedModel);
397
+ }
398
+ }, 100);
399
+ }
400
 
401
  setShowRedesignDialog(false);
402
  setRedesignUrl('');
403
  } else {
404
+ setRedesignError(duplicateResult.message || 'Failed to duplicate space');
405
  }
 
 
406
  }
407
  } catch (error: any) {
408
  console.error('Redesign error:', error);
409
+ setRedesignError(error.response?.data?.message || error.message || 'Failed to process redesign request');
410
  } finally {
411
  setIsRedesigning(false);
412
  }
frontend/src/lib/api.ts CHANGED
@@ -502,6 +502,15 @@ class ApiClient {
502
  return response.data;
503
  }
504
 
 
 
 
 
 
 
 
 
 
505
  logout() {
506
  this.token = null;
507
  }
 
502
  return response.data;
503
  }
504
 
505
+ async duplicateSpace(fromSpaceId: string, toSpaceName?: string, isPrivate: boolean = false): Promise<any> {
506
+ const response = await this.client.post('/api/duplicate-space', {
507
+ from_space_id: fromSpaceId,
508
+ to_space_name: toSpaceName,
509
+ private: isPrivate
510
+ });
511
+ return response.data;
512
+ }
513
+
514
  logout() {
515
  this.token = null;
516
  }