Spaces:
Running
Running
update
Browse files- backend_api.py +108 -0
- backend_deploy.py +70 -0
- frontend/src/components/LandingPage.tsx +85 -36
- frontend/src/lib/api.ts +9 -0
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 |
-
|
|
|
|
| 250 |
|
| 251 |
-
if (
|
| 252 |
-
//
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
} else {
|
| 257 |
-
|
| 258 |
-
const importMessage = `Imported from ${importUrl}`;
|
| 259 |
-
onStart(importMessage, result.language || 'html', selectedModel);
|
| 260 |
}
|
| 261 |
-
|
| 262 |
-
setShowImportDialog(false);
|
| 263 |
-
setImportUrl('');
|
| 264 |
} else {
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 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 |
-
|
| 346 |
-
onStart
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
| 349 |
|
| 350 |
setShowRedesignDialog(false);
|
| 351 |
setRedesignUrl('');
|
| 352 |
} else {
|
| 353 |
-
setRedesignError(
|
| 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
|
| 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 |
}
|