diff --git a/backend/app/urls.py b/backend/app/urls.py index d4ffe8c3..78eadae5 100644 --- a/backend/app/urls.py +++ b/backend/app/urls.py @@ -4,4 +4,6 @@ urlpatterns = [ path('build', views.build, name='build'), path('download', views.download, name='download'), + path('install_block', views.install_block, name='install_block'), + path('installed_blocks', views.installed_blocks, name='installed_blocks'), ] diff --git a/backend/app/views.py b/backend/app/views.py index d6cf79ec..9e6ac6d7 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -6,6 +6,8 @@ from django.views.decorators.csrf import csrf_exempt import json +import os +from django.conf import settings @csrf_exempt def build(request): @@ -20,3 +22,45 @@ def build(request): def download(request): return JsonResponse({'status': 'request received'}) + + +@csrf_exempt +def install_block(request): + try: + data = json.loads(request.body) + block_name = data.get('package', {}).get('name') or data.get('name', 'Untitled') + + custom_blocks_dir = os.path.join(settings.BASE_DIR, 'custom_blocks') + if not os.path.exists(custom_blocks_dir): + os.makedirs(custom_blocks_dir) + + safe_filename = "".join([c for c in block_name if c.isalpha() or c.isdigit() or c in (' ')]).rstrip() + filepath = os.path.join(custom_blocks_dir, f"{safe_filename}.vc3") + + with open(filepath, 'w') as f: + json.dump(data, f, indent=4) + + return JsonResponse({'status': 'success', 'saved_as': os.path.basename(filepath)}) + except Exception as e: + return HttpResponseBadRequest(str(e)) + +def installed_blocks(request): + try: + custom_blocks_dir = os.path.join(settings.BASE_DIR, 'custom_blocks') + if not os.path.exists(custom_blocks_dir): + return JsonResponse({'blocks': []}) + + blocks = [] + for filename in os.listdir(custom_blocks_dir): + if filename.endswith('.vc3'): + filepath = os.path.join(custom_blocks_dir, filename) + try: + with open(filepath, 'r') as f: + block_data = json.load(f) + blocks.append(block_data) + except Exception as e: + print(f"Failed to load block {filename}: {e}") + + return JsonResponse({'blocks': blocks}) + except Exception as e: + return HttpResponseBadRequest(str(e)) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index b4d382a6..4d05197a 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -160,6 +160,11 @@ origin.replace(':4000', ':4002') ]) +# if DEBUG: +# CORS_ALLOWED_ORIGINS = [ +# env.str('VISUAL_CIRCUIT_FRONTEND_HOST'), +# f"{env.str('VISUAL_CIRCUIT_FRONTEND_HOST')}:{DESIRED_PORT}", +# ] + # Allow frontend to read the Content-Disposition header to get the correct .zip filename CORS_EXPOSE_HEADERS = ['Content-Disposition'] - diff --git a/backend/staticfiles/synthesis/main.py b/backend/staticfiles/synthesis/main.py index 49aed123..6bcca9bf 100644 --- a/backend/staticfiles/synthesis/main.py +++ b/backend/staticfiles/synthesis/main.py @@ -50,40 +50,6 @@ def clean_shared_memory(signum, frame, names, processes): sys.exit(0) -def process_wrapper(method, inputs, outputs, parameters, sync): - """ - Wrapper that restores sys.stdin before running a block's main(). - - On macOS / Windows, multiprocessing uses 'spawn' which starts a fresh - Python interpreter where sys.stdin is None. Calling input() in that - state raises EOFError immediately (Issue #359). - - Strategy: - 1. If fd 0 is a real TTY, open /dev/tty so the block gets a proper - interactive terminal (works even inside Docker with -it). - 2. Otherwise fall back to os.fdopen(0) for piped / non-TTY contexts. - 3. If both fail, leave sys.stdin unchanged and let the block handle it. - """ - import sys - import os - - if os.isatty(0): - # fd 0 is connected to a real terminal — give the block full TTY access - try: - sys.stdin = open('/dev/tty', 'r') - except OSError: - # /dev/tty not available (rare); fall back to wrapping fd 0 directly - try: - sys.stdin = os.fdopen(0) - except OSError: - pass # leave sys.stdin as-is; block may not need input() - # If fd 0 is NOT a tty (pipe / redirect / Docker without -it), - # we do NOT restore stdin — input() will raise EOFError, which is the - # correct, expected behaviour in a non-interactive environment. - - method(inputs, outputs, parameters, sync) - - def main(): """ Main function @@ -165,8 +131,8 @@ def main(): freq = block_data[block_id].get("frequency", 30) processes.append( multiprocessing.Process( - target=process_wrapper, - args=(method, inputs, outputs, parameters, Synchronise(1 / (freq if freq != 0 else 30))) + target=method, + args=(inputs, outputs, parameters, Synchronise(1 / (freq if freq != 0 else 30))) ) ) diff --git a/frontend/.env.development b/frontend/.env.development index a1f02554..82d0cf5b 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,3 +1 @@ -REACT_APP_BACKEND_HOST=http://localhost:8080/api/ -HOST=localhost -DANGEROUSLY_DISABLE_HOST_CHECK=true \ No newline at end of file +REACT_APP_BACKEND_HOST=http://localhost:8080/api/ \ No newline at end of file diff --git a/frontend/src/components/dialogs/project-info-dialog.tsx b/frontend/src/components/dialogs/project-info-dialog.tsx index be892b4f..65ebe4f8 100644 --- a/frontend/src/components/dialogs/project-info-dialog.tsx +++ b/frontend/src/components/dialogs/project-info-dialog.tsx @@ -36,15 +36,6 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject, // Tags of the package (comma separated) const [tagsInput, setTags] = useState(tags ? tags.join(', ') : ''); - const ALLOWED_CATEGORIES = [ - "Computer Vision", - "Control Systems", - "Locomotion", - "Machine Learning", - "Utilities", - "ROS2" - ]; - const fileReader = new FileReader(); fileReader.onload = (event) => { if (event.target?.result) { @@ -145,19 +136,14 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject, Category setCategory(event.target.value)} fullWidth - > - {ALLOWED_CATEGORIES.map((cat) => ( - - {cat} - - ))} - + /> Tags (comma separated) diff --git a/frontend/src/components/marketplace/MarketplacePanel.tsx b/frontend/src/components/marketplace/MarketplacePanel.tsx index 4dbd5969..93d2c3d9 100644 --- a/frontend/src/components/marketplace/MarketplacePanel.tsx +++ b/frontend/src/components/marketplace/MarketplacePanel.tsx @@ -96,29 +96,27 @@ const MarketplacePanel: React.FC = ({ open, onClose, edit const isValid = validateMarketplaceBlock(blockData); if (isValid) { - // Save to Local Storage - let installedBlocks = []; try { - const stored = localStorage.getItem('vc_marketplace_blocks'); - if (stored) { - installedBlocks = JSON.parse(stored); - } - } catch (e) { - console.warn("Could not read local storage", e); - } - - // Avoid duplicates by checking package name - const exists = installedBlocks.find((b: any) => b.package && b.package.name === blockData.package.name); - if (!exists) { - installedBlocks.push(blockData); - localStorage.setItem('vc_marketplace_blocks', JSON.stringify(installedBlocks)); + const backendHost = process.env.REACT_APP_BACKEND_HOST || 'http://localhost:8000/api/'; + const endpoint = backendHost.endsWith('/') ? `${backendHost}install_block` : `${backendHost}/install_block`; - // Dispatch event so the MenuBar updates + const installResponse = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(blockData), + }); + + if (!installResponse.ok) { + throw new Error(`Backend error: ${installResponse.status}`); + } window.dispatchEvent(new Event('vc_marketplace_updated')); - alert(`Successfully added ${block.name} to your Downloads Menu!`); - } else { - alert(`${block.name} is already in your Downloads!`); + alert(`Successfully added ${block.name} to your Custom Blocks!`); + } catch (e) { + console.error("Failed to save to backend", e); + alert(`Failed to save ${block.name} to the backend.`); } } else { alert(`Failed to validate block ${block.name}.`); diff --git a/frontend/src/components/menu/index.tsx b/frontend/src/components/menu/index.tsx index 9a0e55d5..a932edfc 100644 --- a/frontend/src/components/menu/index.tsx +++ b/frontend/src/components/menu/index.tsx @@ -38,14 +38,18 @@ function MenuBar(props: MenuBarProps) { const [marketplaceOpen, setMarketplaceOpen] = useState(false); const [downloads, setDownloads] = useState([]); - const loadDownloads = () => { + const loadDownloads = async () => { try { - const stored = localStorage.getItem('vc_marketplace_blocks'); - if (stored) { - setDownloads(JSON.parse(stored)); + const backendHost = process.env.REACT_APP_BACKEND_HOST || 'http://localhost:8000/api/'; + const endpoint = backendHost.endsWith('/') ? `${backendHost}installed_blocks` : `${backendHost}/installed_blocks`; + + const response = await fetch(endpoint); + if (response.ok) { + const data = await response.json(); + setDownloads(data.blocks || []); } } catch (e) { - console.error("Failed to load downloads", e); + console.error("Failed to load downloads from backend", e); } };