Phase 1-7 of Canvas V4 Ralph Loop implementation: Backend: - Add /api/files routes for browsing model files - Add /api/nx routes for NX model introspection - Add NXIntrospector service to discover expressions and extractors - Add health check with database status Frontend: - Add FileBrowser component for selecting .sim/.prt/.fem files - Add IntrospectionPanel to discover expressions and extractors - Update NodeConfigPanel with browse and introspect buttons - Update schema with NODE_HANDLES for proper flow direction - Update validation for correct DesignVar -> Model -> Solver flow - Update useCanvasStore.addNode() to accept custom data Flow correction: Design Variables now connect TO Model (as source), not FROM Model. This matches the actual data flow in optimization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
156 lines
4.5 KiB
Python
156 lines
4.5 KiB
Python
"""
|
|
Files API Routes
|
|
|
|
Provides file browsing capabilities for the Canvas Builder.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Query
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
import os
|
|
|
|
router = APIRouter()
|
|
|
|
# Path to studies root (go up 5 levels from this file)
|
|
_file_path = os.path.abspath(__file__)
|
|
ATOMIZER_ROOT = Path(os.path.normpath(os.path.dirname(os.path.dirname(os.path.dirname(
|
|
os.path.dirname(os.path.dirname(_file_path))
|
|
)))))
|
|
STUDIES_ROOT = ATOMIZER_ROOT / "studies"
|
|
|
|
|
|
@router.get("/list")
|
|
async def list_files(
|
|
path: str = "",
|
|
types: str = ".sim,.prt,.fem,.afem"
|
|
):
|
|
"""
|
|
List files in a directory, filtered by type.
|
|
|
|
Args:
|
|
path: Relative path from studies root (empty for root)
|
|
types: Comma-separated list of file extensions to include
|
|
|
|
Returns:
|
|
List of files and directories with their paths
|
|
"""
|
|
allowed_types = [t.strip().lower() for t in types.split(',') if t.strip()]
|
|
|
|
base_path = STUDIES_ROOT / path if path else STUDIES_ROOT
|
|
|
|
if not base_path.exists():
|
|
return {"files": [], "path": path, "error": "Directory not found"}
|
|
|
|
files = []
|
|
|
|
try:
|
|
for entry in sorted(base_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
|
# Skip hidden files and directories
|
|
if entry.name.startswith('.'):
|
|
continue
|
|
|
|
if entry.is_dir():
|
|
# Include directories
|
|
files.append({
|
|
"name": entry.name,
|
|
"path": str(entry.relative_to(STUDIES_ROOT)).replace("\\", "/"),
|
|
"isDirectory": True,
|
|
})
|
|
else:
|
|
# Include files matching type filter
|
|
suffix = entry.suffix.lower()
|
|
if suffix in allowed_types:
|
|
files.append({
|
|
"name": entry.name,
|
|
"path": str(entry.relative_to(STUDIES_ROOT)).replace("\\", "/"),
|
|
"isDirectory": False,
|
|
"size": entry.stat().st_size,
|
|
})
|
|
except PermissionError:
|
|
return {"files": [], "path": path, "error": "Permission denied"}
|
|
except Exception as e:
|
|
return {"files": [], "path": path, "error": str(e)}
|
|
|
|
return {"files": files, "path": path}
|
|
|
|
|
|
@router.get("/search")
|
|
async def search_files(
|
|
query: str,
|
|
types: str = ".sim,.prt,.fem,.afem",
|
|
max_results: int = 50
|
|
):
|
|
"""
|
|
Search for files by name pattern.
|
|
|
|
Args:
|
|
query: Search pattern (partial name match)
|
|
types: Comma-separated list of file extensions to include
|
|
max_results: Maximum number of results to return
|
|
|
|
Returns:
|
|
List of matching files with their paths
|
|
"""
|
|
allowed_types = [t.strip().lower() for t in types.split(',') if t.strip()]
|
|
query_lower = query.lower()
|
|
|
|
files = []
|
|
|
|
def search_recursive(directory: Path, depth: int = 0):
|
|
"""Recursively search for matching files"""
|
|
if depth > 10 or len(files) >= max_results: # Limit depth and results
|
|
return
|
|
|
|
try:
|
|
for entry in directory.iterdir():
|
|
if len(files) >= max_results:
|
|
return
|
|
|
|
if entry.name.startswith('.'):
|
|
continue
|
|
|
|
if entry.is_dir():
|
|
search_recursive(entry, depth + 1)
|
|
elif entry.suffix.lower() in allowed_types:
|
|
if query_lower in entry.name.lower():
|
|
files.append({
|
|
"name": entry.name,
|
|
"path": str(entry.relative_to(STUDIES_ROOT)).replace("\\", "/"),
|
|
"isDirectory": False,
|
|
"size": entry.stat().st_size,
|
|
})
|
|
except (PermissionError, OSError):
|
|
pass
|
|
|
|
search_recursive(STUDIES_ROOT)
|
|
|
|
return {"files": files, "query": query, "total": len(files)}
|
|
|
|
|
|
@router.get("/exists")
|
|
async def check_file_exists(path: str):
|
|
"""
|
|
Check if a file exists.
|
|
|
|
Args:
|
|
path: Relative path from studies root
|
|
|
|
Returns:
|
|
Boolean indicating if file exists and file info
|
|
"""
|
|
file_path = STUDIES_ROOT / path
|
|
exists = file_path.exists()
|
|
|
|
result = {
|
|
"exists": exists,
|
|
"path": path,
|
|
}
|
|
|
|
if exists:
|
|
result["isDirectory"] = file_path.is_dir()
|
|
if file_path.is_file():
|
|
result["size"] = file_path.stat().st_size
|
|
result["name"] = file_path.name
|
|
|
|
return result
|