Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]
44 changes: 44 additions & 0 deletions backend/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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))
7 changes: 6 additions & 1 deletion backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

38 changes: 2 additions & 36 deletions backend/staticfiles/synthesis/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
)
)

Expand Down
4 changes: 1 addition & 3 deletions frontend/.env.development
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
REACT_APP_BACKEND_HOST=http://localhost:8080/api/
HOST=localhost
DANGEROUSLY_DISABLE_HOST_CHECK=true
REACT_APP_BACKEND_HOST=http://localhost:8080/api/
20 changes: 3 additions & 17 deletions frontend/src/components/dialogs/project-info-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -145,19 +136,14 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
Category
</DialogContentText>
<TextField
select
autoFocus
margin="dense"
type="text"
variant='outlined'
value={categoryInput}
onChange={(event) => setCategory(event.target.value)}
fullWidth
>
{ALLOWED_CATEGORIES.map((cat) => (
<MenuItem key={cat} value={cat}>
{cat}
</MenuItem>
))}
</TextField>
/>

<DialogContentText>
Tags (comma separated)
Expand Down
36 changes: 17 additions & 19 deletions frontend/src/components/marketplace/MarketplacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,27 @@ const MarketplacePanel: React.FC<MarketplacePanelProps> = ({ 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}.`);
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/components/menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ function MenuBar(props: MenuBarProps) {
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
const [downloads, setDownloads] = useState<any[]>([]);

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);
}
};

Expand Down
Loading