docs: Comprehensive documentation update for Dashboard V3 and Canvas

## Documentation Updates
- DASHBOARD.md: Updated to V3.0 with Canvas V3 features, file browser, introspection
- DASHBOARD_IMPLEMENTATION_STATUS.md: Marked Canvas V3 features as COMPLETE
- CANVAS.md: New comprehensive guide for Canvas Builder V3 with all features
- CLAUDE.md: Added dashboard quick reference and Canvas V3 features

## Canvas V3 Features Documented
- File Browser: Browse studies directory for model files
- Model Introspection: Auto-discover expressions, solver type, dependencies
- One-Click Add: Add expressions as design variables instantly
- Claude Bug Fixes: WebSocket reconnection, SQL errors resolved
- Health Check: /api/health endpoint for monitoring

## Backend Services
- NX introspection service with expression discovery
- File browser API with type filtering
- Claude session management improvements
- Context builder enhancements

## Frontend Components
- FileBrowser: Modal for file selection with search
- IntrospectionPanel: View discovered model information
- ExpressionSelector: Dropdown for design variable configuration
- Improved chat hooks with reconnection logic

## Plan Documents
- Added RALPH_LOOP_CANVAS_V2/V3 implementation records
- Added ATOMIZER_DASHBOARD_V2_MASTER_PLAN
- Added investigation and sync documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 20:48:58 -05:00
parent 1c7c7aff05
commit ac5e9b4054
23 changed files with 10860 additions and 773 deletions

View File

@@ -1,16 +1,28 @@
"""
Files API Routes
Provides file browsing capabilities for the Canvas Builder.
Provides file browsing and import capabilities for the Canvas Builder.
Supports importing NX model files from anywhere on the file system.
"""
from fastapi import APIRouter, Query
from fastapi import APIRouter, Query, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from pathlib import Path
from typing import List, Optional
import os
import shutil
import re
router = APIRouter()
class ImportRequest(BaseModel):
"""Request to import a file from a Windows path"""
source_path: str
study_name: str
copy_related: bool = True
# 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(
@@ -153,3 +165,240 @@ async def check_file_exists(path: str):
result["name"] = file_path.name
return result
def find_related_nx_files(source_path: Path) -> List[Path]:
"""
Find all related NX files based on naming conventions.
Given a .sim file like 'model_sim1.sim', finds:
- model.prt (geometry part)
- model_fem1.fem (FEM file)
- model_fem1_i.prt (idealized part)
- model_sim1.sim (simulation)
Args:
source_path: Path to any NX file
Returns:
List of all related file paths that exist
"""
related = []
parent = source_path.parent
stem = source_path.stem
suffix = source_path.suffix.lower()
# Extract base name by removing _sim1, _fem1, _i suffixes
base_name = stem
base_name = re.sub(r'_sim\d*$', '', base_name)
base_name = re.sub(r'_fem\d*$', '', base_name)
base_name = re.sub(r'_i$', '', base_name)
# Define patterns to search for
patterns = [
f"{base_name}.prt", # Main geometry
f"{base_name}_i.prt", # Idealized part
f"{base_name}_fem*.fem", # FEM files
f"{base_name}_fem*_i.prt", # Idealized FEM parts
f"{base_name}_sim*.sim", # Simulation files
f"{base_name}.afem", # Assembled FEM
]
# Search for matching files
for pattern in patterns:
for match in parent.glob(pattern):
if match.exists() and match not in related:
related.append(match)
# Also include the source file itself
if source_path.exists() and source_path not in related:
related.append(source_path)
return related
@router.get("/validate-path")
async def validate_external_path(path: str):
"""
Validate an external Windows path and return info about related files.
Args:
path: Absolute Windows path (e.g., C:\\Models\\bracket.sim)
Returns:
Information about the file and related files
"""
try:
source_path = Path(path)
if not source_path.exists():
return {
"valid": False,
"error": f"Path does not exist: {path}",
}
if not source_path.is_file():
return {
"valid": False,
"error": "Path is not a file",
}
# Check if it's a valid NX file type
valid_extensions = ['.prt', '.sim', '.fem', '.afem']
if source_path.suffix.lower() not in valid_extensions:
return {
"valid": False,
"error": f"Invalid file type. Expected: {', '.join(valid_extensions)}",
}
# Find related files
related = find_related_nx_files(source_path)
return {
"valid": True,
"path": str(source_path),
"name": source_path.name,
"size": source_path.stat().st_size,
"related_files": [
{
"name": f.name,
"path": str(f),
"size": f.stat().st_size,
"type": f.suffix.lower(),
}
for f in related
],
}
except Exception as e:
return {
"valid": False,
"error": str(e),
}
@router.post("/import-from-path")
async def import_from_path(request: ImportRequest):
"""
Import NX model files from an external path into a study folder.
This will:
1. Create the study folder if it doesn't exist
2. Copy the specified file
3. Optionally copy all related files (.prt, .sim, .fem, _i.prt)
Args:
request: ImportRequest with source_path, study_name, and copy_related flag
Returns:
List of imported files
"""
try:
source_path = Path(request.source_path)
if not source_path.exists():
raise HTTPException(status_code=404, detail=f"Source file not found: {request.source_path}")
# Create study folder structure
study_dir = STUDIES_ROOT / request.study_name
model_dir = study_dir / "1_model"
model_dir.mkdir(parents=True, exist_ok=True)
# Find files to copy
if request.copy_related:
files_to_copy = find_related_nx_files(source_path)
else:
files_to_copy = [source_path]
imported = []
for src_file in files_to_copy:
dest_file = model_dir / src_file.name
# Skip if already exists (avoid overwrite)
if dest_file.exists():
imported.append({
"name": src_file.name,
"status": "skipped",
"reason": "Already exists",
"path": str(dest_file.relative_to(STUDIES_ROOT)).replace("\\", "/"),
})
continue
# Copy file
shutil.copy2(src_file, dest_file)
imported.append({
"name": src_file.name,
"status": "imported",
"path": str(dest_file.relative_to(STUDIES_ROOT)).replace("\\", "/"),
"size": dest_file.stat().st_size,
})
return {
"success": True,
"study_name": request.study_name,
"imported_files": imported,
"total_imported": len([f for f in imported if f["status"] == "imported"]),
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/upload")
async def upload_files(
files: List[UploadFile] = File(...),
study_name: str = Query(...),
):
"""
Upload NX model files to a study folder.
Args:
files: List of files to upload
study_name: Target study name
Returns:
List of uploaded files
"""
try:
# Create study folder structure
study_dir = STUDIES_ROOT / study_name
model_dir = study_dir / "1_model"
model_dir.mkdir(parents=True, exist_ok=True)
uploaded = []
for file in files:
# Validate file type
suffix = Path(file.filename).suffix.lower()
if suffix not in ['.prt', '.sim', '.fem', '.afem']:
uploaded.append({
"name": file.filename,
"status": "rejected",
"reason": f"Invalid file type: {suffix}",
})
continue
dest_file = model_dir / file.filename
# Save file
content = await file.read()
with open(dest_file, 'wb') as f:
f.write(content)
uploaded.append({
"name": file.filename,
"status": "uploaded",
"path": str(dest_file.relative_to(STUDIES_ROOT)).replace("\\", "/"),
"size": len(content),
})
return {
"success": True,
"study_name": study_name,
"uploaded_files": uploaded,
"total_uploaded": len([f for f in uploaded if f["status"] == "uploaded"]),
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))