Spaces:
Running
Running
owenkaplinsky
commited on
Commit
·
0daf9fe
1
Parent(s):
7c1461a
Add replace block tool
Browse files- project/chat.py +105 -8
- project/src/index.js +647 -552
project/chat.py
CHANGED
|
@@ -43,6 +43,10 @@ variable_results = {}
|
|
| 43 |
edit_mcp_queue = queue.Queue()
|
| 44 |
edit_mcp_results = {}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
# Global variable to store the deployed HF MCP server URL
|
| 47 |
current_mcp_server_url = None
|
| 48 |
|
|
@@ -269,6 +273,48 @@ def edit_mcp(inputs=None, outputs=None):
|
|
| 269 |
traceback.print_exc()
|
| 270 |
return f"Error editing MCP block: {str(e)}"
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
# Unified Server-Sent Events endpoint for all workspace operations
|
| 273 |
@app.get("/unified_stream")
|
| 274 |
async def unified_stream():
|
|
@@ -352,6 +398,23 @@ async def unified_stream():
|
|
| 352 |
else:
|
| 353 |
print(f"[SSE SKIP] Skipping duplicate request for ID: {request_id}")
|
| 354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
else:
|
| 356 |
# Send a heartbeat every 30 seconds to keep connection alive
|
| 357 |
heartbeat_counter += 1
|
|
@@ -437,6 +500,22 @@ async def edit_mcp_result(request: Request):
|
|
| 437 |
|
| 438 |
return {"received": True}
|
| 439 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
def deploy_to_huggingface(space_name):
|
| 441 |
global stored_hf_key
|
| 442 |
|
|
@@ -888,6 +967,25 @@ def create_gradio_interface():
|
|
| 888 |
}
|
| 889 |
}
|
| 890 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
{
|
| 892 |
"type": "function",
|
| 893 |
"name": "deploy_to_huggingface",
|
|
@@ -1081,14 +1179,6 @@ def create_gradio_interface():
|
|
| 1081 |
tool_result = None
|
| 1082 |
result_label = ""
|
| 1083 |
|
| 1084 |
-
# Log MCP tool calls
|
| 1085 |
-
if function_name not in ("delete_block", "create_block", "create_variable", "edit_mcp", "deploy_to_huggingface"):
|
| 1086 |
-
# This is an MCP tool call
|
| 1087 |
-
print(Fore.GREEN + f"[MCP TOOL CALL] Running MCP with inputs:" + Style.RESET_ALL)
|
| 1088 |
-
print(Fore.GREEN + f" Tool name: {function_name}" + Style.RESET_ALL)
|
| 1089 |
-
for key, value in function_args.items():
|
| 1090 |
-
print(Fore.GREEN + f" {key}: {value}" + Style.RESET_ALL)
|
| 1091 |
-
|
| 1092 |
if function_name == "delete_block":
|
| 1093 |
block_id = function_args.get("id", "")
|
| 1094 |
print(Fore.YELLOW + f"Agent deleted block with ID `{block_id}`." + Style.RESET_ALL)
|
|
@@ -1193,6 +1283,13 @@ def create_gradio_interface():
|
|
| 1193 |
tool_result = edit_mcp(inputs, outputs)
|
| 1194 |
result_label = "Edit MCP Operation"
|
| 1195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1196 |
elif function_name == "deploy_to_huggingface":
|
| 1197 |
space_name = function_args.get("space_name", "")
|
| 1198 |
print(Fore.YELLOW + f"Agent deploying to Hugging Face Space `{space_name}`." + Style.RESET_ALL)
|
|
|
|
| 43 |
edit_mcp_queue = queue.Queue()
|
| 44 |
edit_mcp_results = {}
|
| 45 |
|
| 46 |
+
# Queue for replace block requests and results storage
|
| 47 |
+
replace_block_queue = queue.Queue()
|
| 48 |
+
replace_block_results = {}
|
| 49 |
+
|
| 50 |
# Global variable to store the deployed HF MCP server URL
|
| 51 |
current_mcp_server_url = None
|
| 52 |
|
|
|
|
| 273 |
traceback.print_exc()
|
| 274 |
return f"Error editing MCP block: {str(e)}"
|
| 275 |
|
| 276 |
+
def replace_block(block_id, command):
|
| 277 |
+
try:
|
| 278 |
+
print(f"[REPLACE REQUEST] Attempting to replace block {block_id} with: {command}")
|
| 279 |
+
|
| 280 |
+
# Generate a unique request ID
|
| 281 |
+
request_id = str(uuid.uuid4())
|
| 282 |
+
|
| 283 |
+
# Clear any old results for this request ID first
|
| 284 |
+
if request_id in replace_block_results:
|
| 285 |
+
replace_block_results.pop(request_id)
|
| 286 |
+
|
| 287 |
+
# Build the replace data
|
| 288 |
+
replace_data = {"request_id": request_id, "block_id": block_id, "block_spec": command}
|
| 289 |
+
|
| 290 |
+
# Add to replace block queue
|
| 291 |
+
replace_block_queue.put(replace_data)
|
| 292 |
+
print(f"[REPLACE REQUEST] Added to queue with ID: {request_id}")
|
| 293 |
+
|
| 294 |
+
# Wait for result with timeout
|
| 295 |
+
timeout = 8 # 8 seconds timeout
|
| 296 |
+
start_time = time.time()
|
| 297 |
+
check_interval = 0.05 # Check more frequently
|
| 298 |
+
|
| 299 |
+
while time.time() - start_time < timeout:
|
| 300 |
+
if request_id in replace_block_results:
|
| 301 |
+
result = replace_block_results.pop(request_id)
|
| 302 |
+
print(f"[REPLACE RESULT] Received result for {request_id}: success={result.get('success')}, error={result.get('error')}")
|
| 303 |
+
if result["success"]:
|
| 304 |
+
return f"[TOOL] Successfully replaced block {block_id}"
|
| 305 |
+
else:
|
| 306 |
+
return f"[TOOL] Failed to replace block: {result.get('error', 'Unknown error')}"
|
| 307 |
+
time.sleep(check_interval)
|
| 308 |
+
|
| 309 |
+
print(f"[REPLACE TIMEOUT] No response received for request {request_id} after {timeout} seconds")
|
| 310 |
+
return f"Timeout waiting for block replacement confirmation"
|
| 311 |
+
|
| 312 |
+
except Exception as e:
|
| 313 |
+
print(f"[REPLACE ERROR] {e}")
|
| 314 |
+
import traceback
|
| 315 |
+
traceback.print_exc()
|
| 316 |
+
return f"Error replacing block: {str(e)}"
|
| 317 |
+
|
| 318 |
# Unified Server-Sent Events endpoint for all workspace operations
|
| 319 |
@app.get("/unified_stream")
|
| 320 |
async def unified_stream():
|
|
|
|
| 398 |
else:
|
| 399 |
print(f"[SSE SKIP] Skipping duplicate request for ID: {request_id}")
|
| 400 |
|
| 401 |
+
# Check replace block queue
|
| 402 |
+
elif not replace_block_queue.empty():
|
| 403 |
+
replace_request = replace_block_queue.get_nowait()
|
| 404 |
+
request_id = replace_request.get("request_id")
|
| 405 |
+
request_key = f"replace_{request_id}"
|
| 406 |
+
|
| 407 |
+
# Avoid sending duplicate requests too quickly
|
| 408 |
+
if request_key not in sent_requests:
|
| 409 |
+
sent_requests.add(request_key)
|
| 410 |
+
replace_request["type"] = "replace" # Add type identifier
|
| 411 |
+
yield f"data: {json.dumps(replace_request)}\n\n"
|
| 412 |
+
|
| 413 |
+
# Clear from sent_requests after 10 seconds
|
| 414 |
+
asyncio.create_task(clear_sent_request(sent_requests, request_key, 10))
|
| 415 |
+
else:
|
| 416 |
+
print(f"[SSE SKIP] Skipping duplicate request for ID: {request_id}")
|
| 417 |
+
|
| 418 |
else:
|
| 419 |
# Send a heartbeat every 30 seconds to keep connection alive
|
| 420 |
heartbeat_counter += 1
|
|
|
|
| 500 |
|
| 501 |
return {"received": True}
|
| 502 |
|
| 503 |
+
# Endpoint to receive replace block results from frontend
|
| 504 |
+
@app.post("/replace_block_result")
|
| 505 |
+
async def replace_block_result(request: Request):
|
| 506 |
+
data = await request.json()
|
| 507 |
+
request_id = data.get("request_id")
|
| 508 |
+
success = data.get("success")
|
| 509 |
+
error = data.get("error")
|
| 510 |
+
|
| 511 |
+
print(f"[REPLACE RESULT RECEIVED] request_id={request_id}, success={success}, error={error}")
|
| 512 |
+
|
| 513 |
+
if request_id:
|
| 514 |
+
# Store the result for the replace_block function to retrieve
|
| 515 |
+
replace_block_results[request_id] = data
|
| 516 |
+
|
| 517 |
+
return {"received": True}
|
| 518 |
+
|
| 519 |
def deploy_to_huggingface(space_name):
|
| 520 |
global stored_hf_key
|
| 521 |
|
|
|
|
| 967 |
}
|
| 968 |
}
|
| 969 |
},
|
| 970 |
+
{
|
| 971 |
+
"type": "function",
|
| 972 |
+
"name": "replace_block",
|
| 973 |
+
"description": "Replace a block with a new block in the exact same location. The new block will take the place of the old one.",
|
| 974 |
+
"parameters": {
|
| 975 |
+
"type": "object",
|
| 976 |
+
"properties": {
|
| 977 |
+
"block_id": {
|
| 978 |
+
"type": "string",
|
| 979 |
+
"description": "The ID of the block you want to replace.",
|
| 980 |
+
},
|
| 981 |
+
"command": {
|
| 982 |
+
"type": "string",
|
| 983 |
+
"description": "The create block command using the custom DSL format for the new block. You must rewrite it ENTIRELY from scratch.",
|
| 984 |
+
},
|
| 985 |
+
},
|
| 986 |
+
"required": ["block_id", "command"],
|
| 987 |
+
}
|
| 988 |
+
},
|
| 989 |
{
|
| 990 |
"type": "function",
|
| 991 |
"name": "deploy_to_huggingface",
|
|
|
|
| 1179 |
tool_result = None
|
| 1180 |
result_label = ""
|
| 1181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1182 |
if function_name == "delete_block":
|
| 1183 |
block_id = function_args.get("id", "")
|
| 1184 |
print(Fore.YELLOW + f"Agent deleted block with ID `{block_id}`." + Style.RESET_ALL)
|
|
|
|
| 1283 |
tool_result = edit_mcp(inputs, outputs)
|
| 1284 |
result_label = "Edit MCP Operation"
|
| 1285 |
|
| 1286 |
+
elif function_name == "replace_block":
|
| 1287 |
+
block_id = function_args.get("block_id", "")
|
| 1288 |
+
command = function_args.get("command", "")
|
| 1289 |
+
print(Fore.YELLOW + f"Agent replacing block with ID `{block_id}` with command `{command}`." + Style.RESET_ALL)
|
| 1290 |
+
tool_result = replace_block(block_id, command)
|
| 1291 |
+
result_label = "Replace Block Operation"
|
| 1292 |
+
|
| 1293 |
elif function_name == "deploy_to_huggingface":
|
| 1294 |
space_name = function_args.get("space_name", "")
|
| 1295 |
print(Fore.YELLOW + f"Agent deploying to Hugging Face Space `{space_name}`." + Style.RESET_ALL)
|
project/src/index.js
CHANGED
|
@@ -223,6 +223,557 @@ cleanWorkspace.addEventListener("click", () => {
|
|
| 223 |
ws.cleanUp();
|
| 224 |
});
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
// Set up unified SSE connection for all workspace operations
|
| 227 |
const setupUnifiedStream = () => {
|
| 228 |
const eventSource = new EventSource('/unified_stream');
|
|
@@ -245,6 +796,8 @@ const setupUnifiedStream = () => {
|
|
| 245 |
requestKey = `variable_${data.request_id}`;
|
| 246 |
} else if (data.type === 'edit_mcp') {
|
| 247 |
requestKey = `edit_mcp_${data.request_id}`;
|
|
|
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
// Skip if we've already processed this request
|
|
@@ -341,6 +894,100 @@ const setupUnifiedStream = () => {
|
|
| 341 |
console.error('[SSE] Error sending edit MCP result:', err);
|
| 342 |
});
|
| 343 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
// Handle deletion requests
|
| 345 |
else if (data.type === 'delete' && data.block_id) {
|
| 346 |
console.log('[SSE] Received deletion request for block:', data.block_id);
|
|
@@ -396,558 +1043,6 @@ const setupUnifiedStream = () => {
|
|
| 396 |
let blockId = null;
|
| 397 |
|
| 398 |
try {
|
| 399 |
-
// Parse and create blocks recursively
|
| 400 |
-
function parseAndCreateBlock(spec, shouldPosition = false, placementType = null, placementBlockID = null) {
|
| 401 |
-
// Match block_name(inputs(...)) with proper parenthesis matching
|
| 402 |
-
const blockMatch = spec.match(/^(\w+)\s*\((.*)$/s);
|
| 403 |
-
|
| 404 |
-
if (!blockMatch) {
|
| 405 |
-
throw new Error(`Invalid block specification format: ${spec}`);
|
| 406 |
-
}
|
| 407 |
-
|
| 408 |
-
const blockType = blockMatch[1];
|
| 409 |
-
let content = blockMatch[2].trim();
|
| 410 |
-
|
| 411 |
-
// We need to find the matching closing parenthesis for blockType(
|
| 412 |
-
// Count from the beginning and find where the matching ) is
|
| 413 |
-
let parenCount = 1; // We already have the opening (
|
| 414 |
-
let matchIndex = -1;
|
| 415 |
-
let inQuotes = false;
|
| 416 |
-
let quoteChar = '';
|
| 417 |
-
|
| 418 |
-
for (let i = 0; i < content.length; i++) {
|
| 419 |
-
const char = content[i];
|
| 420 |
-
|
| 421 |
-
// Handle quotes
|
| 422 |
-
if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
|
| 423 |
-
if (!inQuotes) {
|
| 424 |
-
inQuotes = true;
|
| 425 |
-
quoteChar = char;
|
| 426 |
-
} else if (char === quoteChar) {
|
| 427 |
-
inQuotes = false;
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
// Only count parens outside quotes
|
| 432 |
-
if (!inQuotes) {
|
| 433 |
-
if (char === '(') parenCount++;
|
| 434 |
-
else if (char === ')') {
|
| 435 |
-
parenCount--;
|
| 436 |
-
if (parenCount === 0) {
|
| 437 |
-
matchIndex = i;
|
| 438 |
-
break;
|
| 439 |
-
}
|
| 440 |
-
}
|
| 441 |
-
}
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
// Extract content up to the matching closing paren
|
| 445 |
-
if (matchIndex >= 0) {
|
| 446 |
-
content = content.slice(0, matchIndex).trim();
|
| 447 |
-
} else {
|
| 448 |
-
// Fallback: remove last paren if present
|
| 449 |
-
if (content.endsWith(')')) {
|
| 450 |
-
content = content.slice(0, -1).trim();
|
| 451 |
-
}
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
|
| 455 |
-
|
| 456 |
-
// Check if this has inputs() wrapper
|
| 457 |
-
let inputsContent = content;
|
| 458 |
-
if (content.startsWith('inputs(')) {
|
| 459 |
-
// Find the matching closing parenthesis for inputs(
|
| 460 |
-
parenCount = 1;
|
| 461 |
-
matchIndex = -1;
|
| 462 |
-
inQuotes = false;
|
| 463 |
-
quoteChar = '';
|
| 464 |
-
|
| 465 |
-
for (let i = 7; i < content.length; i++) { // Start after 'inputs('
|
| 466 |
-
const char = content[i];
|
| 467 |
-
|
| 468 |
-
// Handle quotes
|
| 469 |
-
if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
|
| 470 |
-
if (!inQuotes) {
|
| 471 |
-
inQuotes = true;
|
| 472 |
-
quoteChar = char;
|
| 473 |
-
} else if (char === quoteChar) {
|
| 474 |
-
inQuotes = false;
|
| 475 |
-
}
|
| 476 |
-
}
|
| 477 |
-
|
| 478 |
-
// Only count parens outside quotes
|
| 479 |
-
if (!inQuotes) {
|
| 480 |
-
if (char === '(') parenCount++;
|
| 481 |
-
else if (char === ')') {
|
| 482 |
-
parenCount--;
|
| 483 |
-
if (parenCount === 0) {
|
| 484 |
-
matchIndex = i;
|
| 485 |
-
break;
|
| 486 |
-
}
|
| 487 |
-
}
|
| 488 |
-
}
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
// Extract content between inputs( and its matching )
|
| 492 |
-
if (matchIndex >= 0) {
|
| 493 |
-
inputsContent = content.slice(7, matchIndex);
|
| 494 |
-
} else {
|
| 495 |
-
// Fallback: remove inputs( and last )
|
| 496 |
-
if (content.endsWith(')')) {
|
| 497 |
-
inputsContent = content.slice(7, -1);
|
| 498 |
-
} else {
|
| 499 |
-
inputsContent = content.slice(7);
|
| 500 |
-
}
|
| 501 |
-
}
|
| 502 |
-
}
|
| 503 |
-
|
| 504 |
-
console.log('[SSE CREATE] inputsContent to parse:', inputsContent);
|
| 505 |
-
|
| 506 |
-
// VALIDATION: Check if trying to place a value block under a statement block
|
| 507 |
-
// Value blocks have an output connection but no previous connection
|
| 508 |
-
if (placementType === 'under') {
|
| 509 |
-
// Check if this block type is a value block by temporarily creating it
|
| 510 |
-
const testBlock = ws.newBlock(blockType);
|
| 511 |
-
const isValueBlock = testBlock.outputConnection && !testBlock.previousConnection;
|
| 512 |
-
testBlock.dispose(true); // Remove the test block
|
| 513 |
-
|
| 514 |
-
if (isValueBlock) {
|
| 515 |
-
throw new Error(`Cannot place value block '${blockType}' under a statement block. Value blocks must be nested inside inputs of other blocks or placed in MCP outputs using type: "input". Try creating a variable and assigning the value to that.`);
|
| 516 |
-
}
|
| 517 |
-
}
|
| 518 |
-
|
| 519 |
-
// Create the block
|
| 520 |
-
const newBlock = ws.newBlock(blockType);
|
| 521 |
-
|
| 522 |
-
if (inputsContent) {
|
| 523 |
-
// Parse the inputs content
|
| 524 |
-
console.log('[SSE CREATE] About to call parseInputs with:', inputsContent);
|
| 525 |
-
const inputs = parseInputs(inputsContent);
|
| 526 |
-
console.log('[SSE CREATE] Parsed inputs:', inputs);
|
| 527 |
-
|
| 528 |
-
// Special handling for make_json block
|
| 529 |
-
if (blockType === 'make_json') {
|
| 530 |
-
// Count FIELD entries to determine how many fields we need
|
| 531 |
-
let fieldCount = 0;
|
| 532 |
-
const fieldValues = {};
|
| 533 |
-
const keyValues = {};
|
| 534 |
-
|
| 535 |
-
for (const [key, value] of Object.entries(inputs)) {
|
| 536 |
-
const fieldMatch = key.match(/^FIELD(\d+)$/);
|
| 537 |
-
const keyMatch = key.match(/^KEY(\d+)$/);
|
| 538 |
-
|
| 539 |
-
if (fieldMatch) {
|
| 540 |
-
const index = parseInt(fieldMatch[1]);
|
| 541 |
-
fieldCount = Math.max(fieldCount, index + 1);
|
| 542 |
-
fieldValues[index] = value;
|
| 543 |
-
} else if (keyMatch) {
|
| 544 |
-
const index = parseInt(keyMatch[1]);
|
| 545 |
-
keyValues[index] = value;
|
| 546 |
-
}
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
// Set up the mutator state
|
| 550 |
-
if (fieldCount > 0) {
|
| 551 |
-
newBlock.fieldCount_ = fieldCount;
|
| 552 |
-
newBlock.fieldKeys_ = [];
|
| 553 |
-
|
| 554 |
-
// Create the inputs through the mutator
|
| 555 |
-
for (let i = 0; i < fieldCount; i++) {
|
| 556 |
-
const keyValue = keyValues[i];
|
| 557 |
-
const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
|
| 558 |
-
? keyValue.replace(/^["']|["']$/g, '')
|
| 559 |
-
: `key${i}`;
|
| 560 |
-
|
| 561 |
-
newBlock.fieldKeys_[i] = key;
|
| 562 |
-
|
| 563 |
-
// Create the input
|
| 564 |
-
const input = newBlock.appendValueInput('FIELD' + i);
|
| 565 |
-
const field = new Blockly.FieldTextInput(key);
|
| 566 |
-
field.setValidator((newValue) => {
|
| 567 |
-
newBlock.fieldKeys_[i] = newValue || `key${i}`;
|
| 568 |
-
return newValue;
|
| 569 |
-
});
|
| 570 |
-
input.appendField(field, 'KEY' + i);
|
| 571 |
-
input.appendField(':');
|
| 572 |
-
}
|
| 573 |
-
|
| 574 |
-
// Now connect the field values
|
| 575 |
-
for (let i = 0; i < fieldCount; i++) {
|
| 576 |
-
const value = fieldValues[i];
|
| 577 |
-
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 578 |
-
// This is a nested block, create it recursively
|
| 579 |
-
const childBlock = parseAndCreateBlock(value);
|
| 580 |
-
|
| 581 |
-
// Connect the child block to the FIELD input
|
| 582 |
-
const input = newBlock.getInput('FIELD' + i);
|
| 583 |
-
if (input && input.connection && childBlock.outputConnection) {
|
| 584 |
-
childBlock.outputConnection.connect(input.connection);
|
| 585 |
-
}
|
| 586 |
-
}
|
| 587 |
-
}
|
| 588 |
-
}
|
| 589 |
-
} else if (blockType === 'text_join') {
|
| 590 |
-
// Special handling for text_join block (and similar blocks with ADD0, ADD1, ADD2...)
|
| 591 |
-
// Count ADD entries to determine how many items we need
|
| 592 |
-
let addCount = 0;
|
| 593 |
-
const addValues = {};
|
| 594 |
-
|
| 595 |
-
for (const [key, value] of Object.entries(inputs)) {
|
| 596 |
-
const addMatch = key.match(/^ADD(\d+)$/);
|
| 597 |
-
if (addMatch) {
|
| 598 |
-
const index = parseInt(addMatch[1]);
|
| 599 |
-
addCount = Math.max(addCount, index + 1);
|
| 600 |
-
addValues[index] = value;
|
| 601 |
-
}
|
| 602 |
-
}
|
| 603 |
-
|
| 604 |
-
console.log('[SSE CREATE] text_join detected with', addCount, 'items');
|
| 605 |
-
|
| 606 |
-
// Store pending text_join state to apply after initSvg()
|
| 607 |
-
if (addCount > 0) {
|
| 608 |
-
newBlock.pendingAddCount_ = addCount;
|
| 609 |
-
newBlock.pendingAddValues_ = addValues;
|
| 610 |
-
}
|
| 611 |
-
} else if (blockType === 'controls_if') {
|
| 612 |
-
// Special handling for if/else blocks - create condition blocks now and store references
|
| 613 |
-
const conditionBlocks = {};
|
| 614 |
-
const conditionBlockObjects = {};
|
| 615 |
-
let hasElse = false;
|
| 616 |
-
|
| 617 |
-
console.log('[SSE CREATE] controls_if inputs:', inputs);
|
| 618 |
-
|
| 619 |
-
// Process condition inputs and store block objects
|
| 620 |
-
// Blockly uses IF0, IF1, IF2... not IF, IFELSEN0, IFELSEN1
|
| 621 |
-
for (const [key, value] of Object.entries(inputs)) {
|
| 622 |
-
if (key.match(/^IF\d+$/)) {
|
| 623 |
-
// This is a condition block specification (IF0, IF1, IF2, ...)
|
| 624 |
-
conditionBlocks[key] = value;
|
| 625 |
-
|
| 626 |
-
if (typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 627 |
-
// Create the condition block now
|
| 628 |
-
const conditionBlock = parseAndCreateBlock(value);
|
| 629 |
-
conditionBlockObjects[key] = conditionBlock;
|
| 630 |
-
console.log('[SSE CREATE] Created condition block for', key);
|
| 631 |
-
}
|
| 632 |
-
} else if (key === 'ELSE' && value === true) {
|
| 633 |
-
// ELSE is a marker with no value (set to true by parseInputs)
|
| 634 |
-
console.log('[SSE CREATE] Detected ELSE marker');
|
| 635 |
-
hasElse = true;
|
| 636 |
-
}
|
| 637 |
-
}
|
| 638 |
-
|
| 639 |
-
// Count IFELSE (else-if) blocks: IF1, IF2, IF3... (IF0 is the main if, not an else-if)
|
| 640 |
-
let elseIfCount = 0;
|
| 641 |
-
for (const key of Object.keys(conditionBlocks)) {
|
| 642 |
-
if (key.match(/^IF\d+$/) && key !== 'IF0') {
|
| 643 |
-
elseIfCount++;
|
| 644 |
-
}
|
| 645 |
-
}
|
| 646 |
-
|
| 647 |
-
console.log('[SSE CREATE] controls_if parsed: elseIfCount =', elseIfCount, 'hasElse =', hasElse);
|
| 648 |
-
|
| 649 |
-
// Store condition block OBJECTS for later - we'll connect them after mutator creates inputs
|
| 650 |
-
newBlock.pendingConditionBlockObjects_ = conditionBlockObjects;
|
| 651 |
-
newBlock.pendingElseifCount_ = elseIfCount;
|
| 652 |
-
newBlock.pendingElseCount_ = hasElse ? 1 : 0;
|
| 653 |
-
console.log('[SSE CREATE] Stored pending condition block objects:', Object.keys(conditionBlockObjects));
|
| 654 |
-
// Skip normal input processing for controls_if - we handle conditions after mutator
|
| 655 |
-
} else if (blockType !== 'controls_if') {
|
| 656 |
-
// Normal block handling (skip for controls_if which is handled specially)
|
| 657 |
-
for (const [key, value] of Object.entries(inputs)) {
|
| 658 |
-
if (typeof value === 'string') {
|
| 659 |
-
// Check if this is a nested block specification
|
| 660 |
-
if (value.match(/^\w+\s*\(inputs\(/)) {
|
| 661 |
-
// This is a nested block, create it recursively
|
| 662 |
-
const childBlock = parseAndCreateBlock(value);
|
| 663 |
-
|
| 664 |
-
// Connect the child block to the appropriate input
|
| 665 |
-
const input = newBlock.getInput(key);
|
| 666 |
-
if (input && input.connection && childBlock.outputConnection) {
|
| 667 |
-
childBlock.outputConnection.connect(input.connection);
|
| 668 |
-
}
|
| 669 |
-
} else {
|
| 670 |
-
// This is a simple value, set it as a field
|
| 671 |
-
// Remove quotes if present
|
| 672 |
-
const cleanValue = value.replace(/^["']|["']$/g, '');
|
| 673 |
-
|
| 674 |
-
// Try to set as a field value
|
| 675 |
-
try {
|
| 676 |
-
newBlock.setFieldValue(cleanValue, key);
|
| 677 |
-
} catch (e) {
|
| 678 |
-
console.log(`[SSE CREATE] Could not set field ${key} to ${cleanValue}:`, e);
|
| 679 |
-
}
|
| 680 |
-
}
|
| 681 |
-
} else if (typeof value === 'number') {
|
| 682 |
-
// Set numeric field value
|
| 683 |
-
try {
|
| 684 |
-
newBlock.setFieldValue(value, key);
|
| 685 |
-
} catch (e) {
|
| 686 |
-
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 687 |
-
}
|
| 688 |
-
} else if (typeof value === 'boolean') {
|
| 689 |
-
// Set boolean field value
|
| 690 |
-
try {
|
| 691 |
-
newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
|
| 692 |
-
} catch (e) {
|
| 693 |
-
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 694 |
-
}
|
| 695 |
-
}
|
| 696 |
-
}
|
| 697 |
-
}
|
| 698 |
-
}
|
| 699 |
-
|
| 700 |
-
// Initialize the block (renders it)
|
| 701 |
-
newBlock.initSvg();
|
| 702 |
-
|
| 703 |
-
// Apply pending controls_if mutations (must be after initSvg)
|
| 704 |
-
try {
|
| 705 |
-
console.log('[SSE CREATE] Checking for controls_if mutations: type =', newBlock.type, 'pendingElseifCount_ =', newBlock.pendingElseifCount_, 'pendingConditionBlockObjects_ =', !!newBlock.pendingConditionBlockObjects_);
|
| 706 |
-
if (newBlock.type === 'controls_if' && (newBlock.pendingElseifCount_ > 0 || newBlock.pendingElseCount_ > 0 || newBlock.pendingConditionBlockObjects_)) {
|
| 707 |
-
console.log('[SSE CREATE] ENTERING controls_if mutation block');
|
| 708 |
-
console.log('[SSE CREATE] Applying controls_if mutation:', {
|
| 709 |
-
elseifCount: newBlock.pendingElseifCount_,
|
| 710 |
-
elseCount: newBlock.pendingElseCount_
|
| 711 |
-
});
|
| 712 |
-
|
| 713 |
-
// Use the loadExtraState method if available (Blockly's preferred way)
|
| 714 |
-
if (typeof newBlock.loadExtraState === 'function') {
|
| 715 |
-
const state = {};
|
| 716 |
-
if (newBlock.pendingElseifCount_ > 0) {
|
| 717 |
-
state.elseIfCount = newBlock.pendingElseifCount_;
|
| 718 |
-
}
|
| 719 |
-
if (newBlock.pendingElseCount_ > 0) {
|
| 720 |
-
state.hasElse = true;
|
| 721 |
-
}
|
| 722 |
-
console.log('[SSE CREATE] Using loadExtraState with:', state);
|
| 723 |
-
newBlock.loadExtraState(state);
|
| 724 |
-
console.log('[SSE CREATE] After loadExtraState');
|
| 725 |
-
} else {
|
| 726 |
-
// Fallback: Set the internal state variables and call updateShape_
|
| 727 |
-
newBlock.elseifCount_ = newBlock.pendingElseifCount_;
|
| 728 |
-
newBlock.elseCount_ = newBlock.pendingElseCount_;
|
| 729 |
-
|
| 730 |
-
if (typeof newBlock.updateShape_ === 'function') {
|
| 731 |
-
console.log('[SSE CREATE] Calling updateShape_ on controls_if');
|
| 732 |
-
newBlock.updateShape_();
|
| 733 |
-
}
|
| 734 |
-
}
|
| 735 |
-
|
| 736 |
-
// Now that the mutator has created all the inputs, connect the stored condition block objects
|
| 737 |
-
console.log('[SSE CREATE] pendingConditionBlockObjects_ exists?', !!newBlock.pendingConditionBlockObjects_);
|
| 738 |
-
if (newBlock.pendingConditionBlockObjects_) {
|
| 739 |
-
const conditionBlockObjects = newBlock.pendingConditionBlockObjects_;
|
| 740 |
-
console.log('[SSE CREATE] Connecting condition blocks:', Object.keys(conditionBlockObjects));
|
| 741 |
-
|
| 742 |
-
// Connect the IF0 condition
|
| 743 |
-
if (conditionBlockObjects['IF0']) {
|
| 744 |
-
const ifBlock = conditionBlockObjects['IF0'];
|
| 745 |
-
const input = newBlock.getInput('IF0');
|
| 746 |
-
console.log('[SSE CREATE] IF0 input exists?', !!input);
|
| 747 |
-
if (input && input.connection && ifBlock.outputConnection) {
|
| 748 |
-
ifBlock.outputConnection.connect(input.connection);
|
| 749 |
-
console.log('[SSE CREATE] Connected IF0 condition');
|
| 750 |
-
} else {
|
| 751 |
-
console.warn('[SSE CREATE] Could not connect IF0 - input:', !!input, 'childConnection:', !!ifBlock.outputConnection);
|
| 752 |
-
}
|
| 753 |
-
}
|
| 754 |
-
|
| 755 |
-
// Connect IF1, IF2, IF3... (else-if conditions)
|
| 756 |
-
console.log('[SSE CREATE] Processing', newBlock.pendingElseifCount_, 'else-if conditions');
|
| 757 |
-
for (let i = 1; i <= newBlock.pendingElseifCount_; i++) {
|
| 758 |
-
const key = 'IF' + i;
|
| 759 |
-
console.log('[SSE CREATE] Looking for key:', key, 'exists?', !!conditionBlockObjects[key]);
|
| 760 |
-
if (conditionBlockObjects[key]) {
|
| 761 |
-
const ifElseBlock = conditionBlockObjects[key];
|
| 762 |
-
const input = newBlock.getInput(key);
|
| 763 |
-
console.log('[SSE CREATE] Input', key, 'exists?', !!input);
|
| 764 |
-
if (input && input.connection && ifElseBlock.outputConnection) {
|
| 765 |
-
ifElseBlock.outputConnection.connect(input.connection);
|
| 766 |
-
console.log('[SSE CREATE] Connected', key, 'condition');
|
| 767 |
-
} else {
|
| 768 |
-
console.warn('[SSE CREATE] Could not connect', key, '- input exists:', !!input, 'has connection:', input ? !!input.connection : false, 'childHasOutput:', !!ifElseBlock.outputConnection);
|
| 769 |
-
}
|
| 770 |
-
}
|
| 771 |
-
}
|
| 772 |
-
} else {
|
| 773 |
-
console.warn('[SSE CREATE] No pendingConditionBlockObjects_ found');
|
| 774 |
-
}
|
| 775 |
-
|
| 776 |
-
// Verify the ELSE input was created
|
| 777 |
-
if (newBlock.pendingElseCount_ > 0) {
|
| 778 |
-
const elseInput = newBlock.getInput('ELSE');
|
| 779 |
-
console.log('[SSE CREATE] ELSE input after mutation:', elseInput);
|
| 780 |
-
if (!elseInput) {
|
| 781 |
-
console.error('[SSE CREATE] ELSE input was NOT created!');
|
| 782 |
-
}
|
| 783 |
-
}
|
| 784 |
-
|
| 785 |
-
// Re-render after connecting condition blocks
|
| 786 |
-
newBlock.render();
|
| 787 |
-
}
|
| 788 |
-
} catch (err) {
|
| 789 |
-
console.error('[SSE CREATE] Error in controls_if mutations:', err);
|
| 790 |
-
}
|
| 791 |
-
|
| 792 |
-
// Apply pending text_join mutations (must be after initSvg)
|
| 793 |
-
if (newBlock.type === 'text_join' && newBlock.pendingAddCount_ && newBlock.pendingAddCount_ > 0) {
|
| 794 |
-
console.log('[SSE CREATE] Applying text_join mutation with', newBlock.pendingAddCount_, 'items');
|
| 795 |
-
|
| 796 |
-
const addCount = newBlock.pendingAddCount_;
|
| 797 |
-
const addValues = newBlock.pendingAddValues_;
|
| 798 |
-
|
| 799 |
-
// Use loadExtraState if available to set the item count
|
| 800 |
-
if (typeof newBlock.loadExtraState === 'function') {
|
| 801 |
-
newBlock.loadExtraState({ itemCount: addCount });
|
| 802 |
-
} else {
|
| 803 |
-
// Fallback: set internal state
|
| 804 |
-
newBlock.itemCount_ = addCount;
|
| 805 |
-
}
|
| 806 |
-
|
| 807 |
-
// Now connect the ADD values
|
| 808 |
-
for (let i = 0; i < addCount; i++) {
|
| 809 |
-
const value = addValues[i];
|
| 810 |
-
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 811 |
-
// This is a nested block, create it recursively
|
| 812 |
-
const childBlock = parseAndCreateBlock(value);
|
| 813 |
-
|
| 814 |
-
// Connect the child block to the ADD input
|
| 815 |
-
const input = newBlock.getInput('ADD' + i);
|
| 816 |
-
if (input && input.connection && childBlock.outputConnection) {
|
| 817 |
-
childBlock.outputConnection.connect(input.connection);
|
| 818 |
-
console.log('[SSE CREATE] Connected ADD' + i + ' input');
|
| 819 |
-
} else {
|
| 820 |
-
console.warn('[SSE CREATE] Could not connect ADD' + i + ' input');
|
| 821 |
-
}
|
| 822 |
-
}
|
| 823 |
-
}
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
-
// Only position the top-level block
|
| 827 |
-
if (shouldPosition) {
|
| 828 |
-
// Find a good position that doesn't overlap existing blocks
|
| 829 |
-
const existingBlocks = ws.getAllBlocks();
|
| 830 |
-
let x = 50;
|
| 831 |
-
let y = 50;
|
| 832 |
-
|
| 833 |
-
// Simple positioning: stack new blocks vertically
|
| 834 |
-
if (existingBlocks.length > 0) {
|
| 835 |
-
const lastBlock = existingBlocks[existingBlocks.length - 1];
|
| 836 |
-
const lastPos = lastBlock.getRelativeToSurfaceXY();
|
| 837 |
-
y = lastPos.y + lastBlock.height + 20;
|
| 838 |
-
}
|
| 839 |
-
|
| 840 |
-
newBlock.moveBy(x, y);
|
| 841 |
-
}
|
| 842 |
-
|
| 843 |
-
// Render the block
|
| 844 |
-
newBlock.render();
|
| 845 |
-
|
| 846 |
-
return newBlock;
|
| 847 |
-
}
|
| 848 |
-
|
| 849 |
-
// Helper function to parse inputs(key: value, key2: value2, ...)
|
| 850 |
-
function parseInputs(inputStr) {
|
| 851 |
-
const result = {};
|
| 852 |
-
let currentKey = '';
|
| 853 |
-
let currentValue = '';
|
| 854 |
-
let depth = 0;
|
| 855 |
-
let inQuotes = false;
|
| 856 |
-
let quoteChar = '';
|
| 857 |
-
let readingKey = true;
|
| 858 |
-
|
| 859 |
-
for (let i = 0; i < inputStr.length; i++) {
|
| 860 |
-
const char = inputStr[i];
|
| 861 |
-
|
| 862 |
-
// Handle quotes
|
| 863 |
-
if ((char === '"' || char === "'") && (i === 0 || inputStr[i - 1] !== '\\')) {
|
| 864 |
-
if (!inQuotes) {
|
| 865 |
-
inQuotes = true;
|
| 866 |
-
quoteChar = char;
|
| 867 |
-
} else if (char === quoteChar) {
|
| 868 |
-
inQuotes = false;
|
| 869 |
-
quoteChar = '';
|
| 870 |
-
}
|
| 871 |
-
}
|
| 872 |
-
|
| 873 |
-
// Handle parentheses depth (for nested blocks)
|
| 874 |
-
if (!inQuotes) {
|
| 875 |
-
if (char === '(') depth++;
|
| 876 |
-
else if (char === ')') depth--;
|
| 877 |
-
}
|
| 878 |
-
|
| 879 |
-
// Handle key-value separation
|
| 880 |
-
if (char === ':' && depth === 0 && !inQuotes && readingKey) {
|
| 881 |
-
readingKey = false;
|
| 882 |
-
currentKey = currentKey.trim();
|
| 883 |
-
continue;
|
| 884 |
-
}
|
| 885 |
-
|
| 886 |
-
// Handle comma separation
|
| 887 |
-
if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
|
| 888 |
-
// Store the key-value pair
|
| 889 |
-
currentValue = currentValue.trim();
|
| 890 |
-
|
| 891 |
-
// Parse the value
|
| 892 |
-
if (currentValue.match(/^\w+\s*\(inputs\(/)) {
|
| 893 |
-
// This is a nested block
|
| 894 |
-
result[currentKey] = currentValue;
|
| 895 |
-
} else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
|
| 896 |
-
// This is a number
|
| 897 |
-
result[currentKey] = parseFloat(currentValue);
|
| 898 |
-
} else if (currentValue === 'true' || currentValue === 'false') {
|
| 899 |
-
// This is a boolean
|
| 900 |
-
result[currentKey] = currentValue === 'true';
|
| 901 |
-
} else {
|
| 902 |
-
// This is a string (remove quotes if present)
|
| 903 |
-
result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
|
| 904 |
-
}
|
| 905 |
-
|
| 906 |
-
// Reset for next key-value pair
|
| 907 |
-
currentKey = '';
|
| 908 |
-
currentValue = '';
|
| 909 |
-
readingKey = true;
|
| 910 |
-
continue;
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
// Accumulate characters
|
| 914 |
-
if (readingKey) {
|
| 915 |
-
currentKey += char;
|
| 916 |
-
} else {
|
| 917 |
-
currentValue += char;
|
| 918 |
-
}
|
| 919 |
-
}
|
| 920 |
-
|
| 921 |
-
// Handle the last key-value pair
|
| 922 |
-
if (currentKey) {
|
| 923 |
-
currentKey = currentKey.trim();
|
| 924 |
-
|
| 925 |
-
// If there's no value, this is a flag/marker (like ELSE)
|
| 926 |
-
if (!currentValue) {
|
| 927 |
-
result[currentKey] = true; // Mark it as present
|
| 928 |
-
} else {
|
| 929 |
-
currentValue = currentValue.trim();
|
| 930 |
-
|
| 931 |
-
// Parse the value
|
| 932 |
-
if (currentValue.match(/^\w+\s*\(inputs\(/)) {
|
| 933 |
-
// This is a nested block
|
| 934 |
-
result[currentKey] = currentValue;
|
| 935 |
-
} else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
|
| 936 |
-
// This is a number
|
| 937 |
-
result[currentKey] = parseFloat(currentValue);
|
| 938 |
-
} else if (currentValue === 'true' || currentValue === 'false') {
|
| 939 |
-
// This is a boolean
|
| 940 |
-
result[currentKey] = currentValue === 'true';
|
| 941 |
-
} else {
|
| 942 |
-
// This is a string (remove quotes if present)
|
| 943 |
-
result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
|
| 944 |
-
}
|
| 945 |
-
}
|
| 946 |
-
}
|
| 947 |
-
|
| 948 |
-
return result;
|
| 949 |
-
}
|
| 950 |
-
|
| 951 |
// Create the block and all its nested children
|
| 952 |
const newBlock = parseAndCreateBlock(data.block_spec, true, data.placement_type, data.blockID);
|
| 953 |
|
|
|
|
| 223 |
ws.cleanUp();
|
| 224 |
});
|
| 225 |
|
| 226 |
+
function parseAndCreateBlock(spec, shouldPosition = false, placementType = null, placementBlockID = null) {
|
| 227 |
+
// Match block_name(inputs(...)) with proper parenthesis matching
|
| 228 |
+
const blockMatch = spec.match(/^(\w+)\s*\((.*)$/s);
|
| 229 |
+
|
| 230 |
+
if (!blockMatch) {
|
| 231 |
+
throw new Error(`Invalid block specification format: ${spec}`);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
const blockType = blockMatch[1];
|
| 235 |
+
let content = blockMatch[2].trim();
|
| 236 |
+
|
| 237 |
+
// We need to find the matching closing parenthesis for blockType(
|
| 238 |
+
// Count from the beginning and find where the matching ) is
|
| 239 |
+
let parenCount = 1; // We already have the opening (
|
| 240 |
+
let matchIndex = -1;
|
| 241 |
+
let inQuotes = false;
|
| 242 |
+
let quoteChar = '';
|
| 243 |
+
|
| 244 |
+
for (let i = 0; i < content.length; i++) {
|
| 245 |
+
const char = content[i];
|
| 246 |
+
|
| 247 |
+
// Handle quotes
|
| 248 |
+
if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
|
| 249 |
+
if (!inQuotes) {
|
| 250 |
+
inQuotes = true;
|
| 251 |
+
quoteChar = char;
|
| 252 |
+
} else if (char === quoteChar) {
|
| 253 |
+
inQuotes = false;
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
// Only count parens outside quotes
|
| 258 |
+
if (!inQuotes) {
|
| 259 |
+
if (char === '(') parenCount++;
|
| 260 |
+
else if (char === ')') {
|
| 261 |
+
parenCount--;
|
| 262 |
+
if (parenCount === 0) {
|
| 263 |
+
matchIndex = i;
|
| 264 |
+
break;
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// Extract content up to the matching closing paren
|
| 271 |
+
if (matchIndex >= 0) {
|
| 272 |
+
content = content.slice(0, matchIndex).trim();
|
| 273 |
+
} else {
|
| 274 |
+
// Fallback: remove last paren if present
|
| 275 |
+
if (content.endsWith(')')) {
|
| 276 |
+
content = content.slice(0, -1).trim();
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
|
| 281 |
+
|
| 282 |
+
// Check if this has inputs() wrapper
|
| 283 |
+
let inputsContent = content;
|
| 284 |
+
if (content.startsWith('inputs(')) {
|
| 285 |
+
// Find the matching closing parenthesis for inputs(
|
| 286 |
+
parenCount = 1;
|
| 287 |
+
matchIndex = -1;
|
| 288 |
+
inQuotes = false;
|
| 289 |
+
quoteChar = '';
|
| 290 |
+
|
| 291 |
+
for (let i = 7; i < content.length; i++) { // Start after 'inputs('
|
| 292 |
+
const char = content[i];
|
| 293 |
+
|
| 294 |
+
// Handle quotes
|
| 295 |
+
if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
|
| 296 |
+
if (!inQuotes) {
|
| 297 |
+
inQuotes = true;
|
| 298 |
+
quoteChar = char;
|
| 299 |
+
} else if (char === quoteChar) {
|
| 300 |
+
inQuotes = false;
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// Only count parens outside quotes
|
| 305 |
+
if (!inQuotes) {
|
| 306 |
+
if (char === '(') parenCount++;
|
| 307 |
+
else if (char === ')') {
|
| 308 |
+
parenCount--;
|
| 309 |
+
if (parenCount === 0) {
|
| 310 |
+
matchIndex = i;
|
| 311 |
+
break;
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// Extract content between inputs( and its matching )
|
| 318 |
+
if (matchIndex >= 0) {
|
| 319 |
+
inputsContent = content.slice(7, matchIndex);
|
| 320 |
+
} else {
|
| 321 |
+
// Fallback: remove inputs( and last )
|
| 322 |
+
if (content.endsWith(')')) {
|
| 323 |
+
inputsContent = content.slice(7, -1);
|
| 324 |
+
} else {
|
| 325 |
+
inputsContent = content.slice(7);
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
console.log('[SSE CREATE] inputsContent to parse:', inputsContent);
|
| 331 |
+
|
| 332 |
+
// VALIDATION: Check if trying to place a value block under a statement block
|
| 333 |
+
// Value blocks have an output connection but no previous connection
|
| 334 |
+
if (placementType === 'under') {
|
| 335 |
+
// Check if this block type is a value block by temporarily creating it
|
| 336 |
+
const testBlock = ws.newBlock(blockType);
|
| 337 |
+
const isValueBlock = testBlock.outputConnection && !testBlock.previousConnection;
|
| 338 |
+
testBlock.dispose(true); // Remove the test block
|
| 339 |
+
|
| 340 |
+
if (isValueBlock) {
|
| 341 |
+
throw new Error(`Cannot place value block '${blockType}' under a statement block. Value blocks must be nested inside inputs of other blocks or placed in MCP outputs using type: "input". Try creating a variable and assigning the value to that.`);
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// Create the block
|
| 346 |
+
const newBlock = ws.newBlock(blockType);
|
| 347 |
+
|
| 348 |
+
if (inputsContent) {
|
| 349 |
+
// Parse the inputs content
|
| 350 |
+
console.log('[SSE CREATE] About to call parseInputs with:', inputsContent);
|
| 351 |
+
const inputs = parseInputs(inputsContent);
|
| 352 |
+
console.log('[SSE CREATE] Parsed inputs:', inputs);
|
| 353 |
+
|
| 354 |
+
// Special handling for make_json block
|
| 355 |
+
if (blockType === 'make_json') {
|
| 356 |
+
// Count FIELD entries to determine how many fields we need
|
| 357 |
+
let fieldCount = 0;
|
| 358 |
+
const fieldValues = {};
|
| 359 |
+
const keyValues = {};
|
| 360 |
+
|
| 361 |
+
for (const [key, value] of Object.entries(inputs)) {
|
| 362 |
+
const fieldMatch = key.match(/^FIELD(\d+)$/);
|
| 363 |
+
const keyMatch = key.match(/^KEY(\d+)$/);
|
| 364 |
+
|
| 365 |
+
if (fieldMatch) {
|
| 366 |
+
const index = parseInt(fieldMatch[1]);
|
| 367 |
+
fieldCount = Math.max(fieldCount, index + 1);
|
| 368 |
+
fieldValues[index] = value;
|
| 369 |
+
} else if (keyMatch) {
|
| 370 |
+
const index = parseInt(keyMatch[1]);
|
| 371 |
+
keyValues[index] = value;
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Set up the mutator state
|
| 376 |
+
if (fieldCount > 0) {
|
| 377 |
+
newBlock.fieldCount_ = fieldCount;
|
| 378 |
+
newBlock.fieldKeys_ = [];
|
| 379 |
+
|
| 380 |
+
// Create the inputs through the mutator
|
| 381 |
+
for (let i = 0; i < fieldCount; i++) {
|
| 382 |
+
const keyValue = keyValues[i];
|
| 383 |
+
const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
|
| 384 |
+
? keyValue.replace(/^["']|["']$/g, '')
|
| 385 |
+
: `key${i}`;
|
| 386 |
+
|
| 387 |
+
newBlock.fieldKeys_[i] = key;
|
| 388 |
+
|
| 389 |
+
// Create the input
|
| 390 |
+
const input = newBlock.appendValueInput('FIELD' + i);
|
| 391 |
+
const field = new Blockly.FieldTextInput(key);
|
| 392 |
+
field.setValidator((newValue) => {
|
| 393 |
+
newBlock.fieldKeys_[i] = newValue || `key${i}`;
|
| 394 |
+
return newValue;
|
| 395 |
+
});
|
| 396 |
+
input.appendField(field, 'KEY' + i);
|
| 397 |
+
input.appendField(':');
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Now connect the field values
|
| 401 |
+
for (let i = 0; i < fieldCount; i++) {
|
| 402 |
+
const value = fieldValues[i];
|
| 403 |
+
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 404 |
+
// This is a nested block, create it recursively
|
| 405 |
+
const childBlock = parseAndCreateBlock(value);
|
| 406 |
+
|
| 407 |
+
// Connect the child block to the FIELD input
|
| 408 |
+
const input = newBlock.getInput('FIELD' + i);
|
| 409 |
+
if (input && input.connection && childBlock.outputConnection) {
|
| 410 |
+
childBlock.outputConnection.connect(input.connection);
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
} else if (blockType === 'text_join') {
|
| 416 |
+
// Special handling for text_join block (and similar blocks with ADD0, ADD1, ADD2...)
|
| 417 |
+
// Count ADD entries to determine how many items we need
|
| 418 |
+
let addCount = 0;
|
| 419 |
+
const addValues = {};
|
| 420 |
+
|
| 421 |
+
for (const [key, value] of Object.entries(inputs)) {
|
| 422 |
+
const addMatch = key.match(/^ADD(\d+)$/);
|
| 423 |
+
if (addMatch) {
|
| 424 |
+
const index = parseInt(addMatch[1]);
|
| 425 |
+
addCount = Math.max(addCount, index + 1);
|
| 426 |
+
addValues[index] = value;
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
console.log('[SSE CREATE] text_join detected with', addCount, 'items');
|
| 431 |
+
|
| 432 |
+
// Store pending text_join state to apply after initSvg()
|
| 433 |
+
if (addCount > 0) {
|
| 434 |
+
newBlock.pendingAddCount_ = addCount;
|
| 435 |
+
newBlock.pendingAddValues_ = addValues;
|
| 436 |
+
}
|
| 437 |
+
} else if (blockType === 'controls_if') {
|
| 438 |
+
// Special handling for if/else blocks - create condition blocks now and store references
|
| 439 |
+
const conditionBlocks = {};
|
| 440 |
+
const conditionBlockObjects = {};
|
| 441 |
+
let hasElse = false;
|
| 442 |
+
|
| 443 |
+
console.log('[SSE CREATE] controls_if inputs:', inputs);
|
| 444 |
+
|
| 445 |
+
// Process condition inputs and store block objects
|
| 446 |
+
// Blockly uses IF0, IF1, IF2... not IF, IFELSEN0, IFELSEN1
|
| 447 |
+
for (const [key, value] of Object.entries(inputs)) {
|
| 448 |
+
if (key.match(/^IF\d+$/)) {
|
| 449 |
+
// This is a condition block specification (IF0, IF1, IF2, ...)
|
| 450 |
+
conditionBlocks[key] = value;
|
| 451 |
+
|
| 452 |
+
if (typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 453 |
+
// Create the condition block now
|
| 454 |
+
const conditionBlock = parseAndCreateBlock(value);
|
| 455 |
+
conditionBlockObjects[key] = conditionBlock;
|
| 456 |
+
console.log('[SSE CREATE] Created condition block for', key);
|
| 457 |
+
}
|
| 458 |
+
} else if (key === 'ELSE' && value === true) {
|
| 459 |
+
// ELSE is a marker with no value (set to true by parseInputs)
|
| 460 |
+
console.log('[SSE CREATE] Detected ELSE marker');
|
| 461 |
+
hasElse = true;
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// Count IFELSE (else-if) blocks: IF1, IF2, IF3... (IF0 is the main if, not an else-if)
|
| 466 |
+
let elseIfCount = 0;
|
| 467 |
+
for (const key of Object.keys(conditionBlocks)) {
|
| 468 |
+
if (key.match(/^IF\d+$/) && key !== 'IF0') {
|
| 469 |
+
elseIfCount++;
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
console.log('[SSE CREATE] controls_if parsed: elseIfCount =', elseIfCount, 'hasElse =', hasElse);
|
| 474 |
+
|
| 475 |
+
// Store condition block OBJECTS for later - we'll connect them after mutator creates inputs
|
| 476 |
+
newBlock.pendingConditionBlockObjects_ = conditionBlockObjects;
|
| 477 |
+
newBlock.pendingElseifCount_ = elseIfCount;
|
| 478 |
+
newBlock.pendingElseCount_ = hasElse ? 1 : 0;
|
| 479 |
+
console.log('[SSE CREATE] Stored pending condition block objects:', Object.keys(conditionBlockObjects));
|
| 480 |
+
// Skip normal input processing for controls_if - we handle conditions after mutator
|
| 481 |
+
} else if (blockType !== 'controls_if') {
|
| 482 |
+
// Normal block handling (skip for controls_if which is handled specially)
|
| 483 |
+
for (const [key, value] of Object.entries(inputs)) {
|
| 484 |
+
if (typeof value === 'string') {
|
| 485 |
+
// Check if this is a nested block specification
|
| 486 |
+
if (value.match(/^\w+\s*\(inputs\(/)) {
|
| 487 |
+
// This is a nested block, create it recursively
|
| 488 |
+
const childBlock = parseAndCreateBlock(value);
|
| 489 |
+
|
| 490 |
+
// Connect the child block to the appropriate input
|
| 491 |
+
const input = newBlock.getInput(key);
|
| 492 |
+
if (input && input.connection && childBlock.outputConnection) {
|
| 493 |
+
childBlock.outputConnection.connect(input.connection);
|
| 494 |
+
}
|
| 495 |
+
} else {
|
| 496 |
+
// This is a simple value, set it as a field
|
| 497 |
+
// Remove quotes if present
|
| 498 |
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
| 499 |
+
|
| 500 |
+
// Try to set as a field value
|
| 501 |
+
try {
|
| 502 |
+
newBlock.setFieldValue(cleanValue, key);
|
| 503 |
+
} catch (e) {
|
| 504 |
+
console.log(`[SSE CREATE] Could not set field ${key} to ${cleanValue}:`, e);
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
} else if (typeof value === 'number') {
|
| 508 |
+
// Set numeric field value
|
| 509 |
+
try {
|
| 510 |
+
newBlock.setFieldValue(value, key);
|
| 511 |
+
} catch (e) {
|
| 512 |
+
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 513 |
+
}
|
| 514 |
+
} else if (typeof value === 'boolean') {
|
| 515 |
+
// Set boolean field value
|
| 516 |
+
try {
|
| 517 |
+
newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
|
| 518 |
+
} catch (e) {
|
| 519 |
+
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
}
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// Initialize the block (renders it)
|
| 527 |
+
newBlock.initSvg();
|
| 528 |
+
|
| 529 |
+
// Apply pending controls_if mutations (must be after initSvg)
|
| 530 |
+
try {
|
| 531 |
+
console.log('[SSE CREATE] Checking for controls_if mutations: type =', newBlock.type, 'pendingElseifCount_ =', newBlock.pendingElseifCount_, 'pendingConditionBlockObjects_ =', !!newBlock.pendingConditionBlockObjects_);
|
| 532 |
+
if (newBlock.type === 'controls_if' && (newBlock.pendingElseifCount_ > 0 || newBlock.pendingElseCount_ > 0 || newBlock.pendingConditionBlockObjects_)) {
|
| 533 |
+
console.log('[SSE CREATE] ENTERING controls_if mutation block');
|
| 534 |
+
console.log('[SSE CREATE] Applying controls_if mutation:', {
|
| 535 |
+
elseifCount: newBlock.pendingElseifCount_,
|
| 536 |
+
elseCount: newBlock.pendingElseCount_
|
| 537 |
+
});
|
| 538 |
+
|
| 539 |
+
// Use the loadExtraState method if available (Blockly's preferred way)
|
| 540 |
+
if (typeof newBlock.loadExtraState === 'function') {
|
| 541 |
+
const state = {};
|
| 542 |
+
if (newBlock.pendingElseifCount_ > 0) {
|
| 543 |
+
state.elseIfCount = newBlock.pendingElseifCount_;
|
| 544 |
+
}
|
| 545 |
+
if (newBlock.pendingElseCount_ > 0) {
|
| 546 |
+
state.hasElse = true;
|
| 547 |
+
}
|
| 548 |
+
console.log('[SSE CREATE] Using loadExtraState with:', state);
|
| 549 |
+
newBlock.loadExtraState(state);
|
| 550 |
+
console.log('[SSE CREATE] After loadExtraState');
|
| 551 |
+
} else {
|
| 552 |
+
// Fallback: Set the internal state variables and call updateShape_
|
| 553 |
+
newBlock.elseifCount_ = newBlock.pendingElseifCount_;
|
| 554 |
+
newBlock.elseCount_ = newBlock.pendingElseCount_;
|
| 555 |
+
|
| 556 |
+
if (typeof newBlock.updateShape_ === 'function') {
|
| 557 |
+
console.log('[SSE CREATE] Calling updateShape_ on controls_if');
|
| 558 |
+
newBlock.updateShape_();
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
// Now that the mutator has created all the inputs, connect the stored condition block objects
|
| 563 |
+
console.log('[SSE CREATE] pendingConditionBlockObjects_ exists?', !!newBlock.pendingConditionBlockObjects_);
|
| 564 |
+
if (newBlock.pendingConditionBlockObjects_) {
|
| 565 |
+
const conditionBlockObjects = newBlock.pendingConditionBlockObjects_;
|
| 566 |
+
console.log('[SSE CREATE] Connecting condition blocks:', Object.keys(conditionBlockObjects));
|
| 567 |
+
|
| 568 |
+
// Connect the IF0 condition
|
| 569 |
+
if (conditionBlockObjects['IF0']) {
|
| 570 |
+
const ifBlock = conditionBlockObjects['IF0'];
|
| 571 |
+
const input = newBlock.getInput('IF0');
|
| 572 |
+
console.log('[SSE CREATE] IF0 input exists?', !!input);
|
| 573 |
+
if (input && input.connection && ifBlock.outputConnection) {
|
| 574 |
+
ifBlock.outputConnection.connect(input.connection);
|
| 575 |
+
console.log('[SSE CREATE] Connected IF0 condition');
|
| 576 |
+
} else {
|
| 577 |
+
console.warn('[SSE CREATE] Could not connect IF0 - input:', !!input, 'childConnection:', !!ifBlock.outputConnection);
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
// Connect IF1, IF2, IF3... (else-if conditions)
|
| 582 |
+
console.log('[SSE CREATE] Processing', newBlock.pendingElseifCount_, 'else-if conditions');
|
| 583 |
+
for (let i = 1; i <= newBlock.pendingElseifCount_; i++) {
|
| 584 |
+
const key = 'IF' + i;
|
| 585 |
+
console.log('[SSE CREATE] Looking for key:', key, 'exists?', !!conditionBlockObjects[key]);
|
| 586 |
+
if (conditionBlockObjects[key]) {
|
| 587 |
+
const ifElseBlock = conditionBlockObjects[key];
|
| 588 |
+
const input = newBlock.getInput(key);
|
| 589 |
+
console.log('[SSE CREATE] Input', key, 'exists?', !!input);
|
| 590 |
+
if (input && input.connection && ifElseBlock.outputConnection) {
|
| 591 |
+
ifElseBlock.outputConnection.connect(input.connection);
|
| 592 |
+
console.log('[SSE CREATE] Connected', key, 'condition');
|
| 593 |
+
} else {
|
| 594 |
+
console.warn('[SSE CREATE] Could not connect', key, '- input exists:', !!input, 'has connection:', input ? !!input.connection : false, 'childHasOutput:', !!ifElseBlock.outputConnection);
|
| 595 |
+
}
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
} else {
|
| 599 |
+
console.warn('[SSE CREATE] No pendingConditionBlockObjects_ found');
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
// Verify the ELSE input was created
|
| 603 |
+
if (newBlock.pendingElseCount_ > 0) {
|
| 604 |
+
const elseInput = newBlock.getInput('ELSE');
|
| 605 |
+
console.log('[SSE CREATE] ELSE input after mutation:', elseInput);
|
| 606 |
+
if (!elseInput) {
|
| 607 |
+
console.error('[SSE CREATE] ELSE input was NOT created!');
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
// Re-render after connecting condition blocks
|
| 612 |
+
newBlock.render();
|
| 613 |
+
}
|
| 614 |
+
} catch (err) {
|
| 615 |
+
console.error('[SSE CREATE] Error in controls_if mutations:', err);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
// Apply pending text_join mutations (must be after initSvg)
|
| 619 |
+
if (newBlock.type === 'text_join' && newBlock.pendingAddCount_ && newBlock.pendingAddCount_ > 0) {
|
| 620 |
+
console.log('[SSE CREATE] Applying text_join mutation with', newBlock.pendingAddCount_, 'items');
|
| 621 |
+
|
| 622 |
+
const addCount = newBlock.pendingAddCount_;
|
| 623 |
+
const addValues = newBlock.pendingAddValues_;
|
| 624 |
+
|
| 625 |
+
// Use loadExtraState if available to set the item count
|
| 626 |
+
if (typeof newBlock.loadExtraState === 'function') {
|
| 627 |
+
newBlock.loadExtraState({ itemCount: addCount });
|
| 628 |
+
} else {
|
| 629 |
+
// Fallback: set internal state
|
| 630 |
+
newBlock.itemCount_ = addCount;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
// Now connect the ADD values
|
| 634 |
+
for (let i = 0; i < addCount; i++) {
|
| 635 |
+
const value = addValues[i];
|
| 636 |
+
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 637 |
+
// This is a nested block, create it recursively
|
| 638 |
+
const childBlock = parseAndCreateBlock(value);
|
| 639 |
+
|
| 640 |
+
// Connect the child block to the ADD input
|
| 641 |
+
const input = newBlock.getInput('ADD' + i);
|
| 642 |
+
if (input && input.connection && childBlock.outputConnection) {
|
| 643 |
+
childBlock.outputConnection.connect(input.connection);
|
| 644 |
+
console.log('[SSE CREATE] Connected ADD' + i + ' input');
|
| 645 |
+
} else {
|
| 646 |
+
console.warn('[SSE CREATE] Could not connect ADD' + i + ' input');
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
// Only position the top-level block
|
| 653 |
+
if (shouldPosition) {
|
| 654 |
+
// Find a good position that doesn't overlap existing blocks
|
| 655 |
+
const existingBlocks = ws.getAllBlocks();
|
| 656 |
+
let x = 50;
|
| 657 |
+
let y = 50;
|
| 658 |
+
|
| 659 |
+
// Simple positioning: stack new blocks vertically
|
| 660 |
+
if (existingBlocks.length > 0) {
|
| 661 |
+
const lastBlock = existingBlocks[existingBlocks.length - 1];
|
| 662 |
+
const lastPos = lastBlock.getRelativeToSurfaceXY();
|
| 663 |
+
y = lastPos.y + lastBlock.height + 20;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
newBlock.moveBy(x, y);
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
// Render the block
|
| 670 |
+
newBlock.render();
|
| 671 |
+
|
| 672 |
+
return newBlock;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
// Helper function to parse inputs(key: value, key2: value2, ...)
|
| 676 |
+
function parseInputs(inputStr) {
|
| 677 |
+
const result = {};
|
| 678 |
+
let currentKey = '';
|
| 679 |
+
let currentValue = '';
|
| 680 |
+
let depth = 0;
|
| 681 |
+
let inQuotes = false;
|
| 682 |
+
let quoteChar = '';
|
| 683 |
+
let readingKey = true;
|
| 684 |
+
|
| 685 |
+
for (let i = 0; i < inputStr.length; i++) {
|
| 686 |
+
const char = inputStr[i];
|
| 687 |
+
|
| 688 |
+
// Handle quotes
|
| 689 |
+
if ((char === '"' || char === "'") && (i === 0 || inputStr[i - 1] !== '\\')) {
|
| 690 |
+
if (!inQuotes) {
|
| 691 |
+
inQuotes = true;
|
| 692 |
+
quoteChar = char;
|
| 693 |
+
} else if (char === quoteChar) {
|
| 694 |
+
inQuotes = false;
|
| 695 |
+
quoteChar = '';
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
// Handle parentheses depth (for nested blocks)
|
| 700 |
+
if (!inQuotes) {
|
| 701 |
+
if (char === '(') depth++;
|
| 702 |
+
else if (char === ')') depth--;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
// Handle key-value separation
|
| 706 |
+
if (char === ':' && depth === 0 && !inQuotes && readingKey) {
|
| 707 |
+
readingKey = false;
|
| 708 |
+
currentKey = currentKey.trim();
|
| 709 |
+
continue;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// Handle comma separation
|
| 713 |
+
if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
|
| 714 |
+
// Store the key-value pair
|
| 715 |
+
currentValue = currentValue.trim();
|
| 716 |
+
|
| 717 |
+
// Parse the value
|
| 718 |
+
if (currentValue.match(/^\w+\s*\(inputs\(/)) {
|
| 719 |
+
// This is a nested block
|
| 720 |
+
result[currentKey] = currentValue;
|
| 721 |
+
} else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
|
| 722 |
+
// This is a number
|
| 723 |
+
result[currentKey] = parseFloat(currentValue);
|
| 724 |
+
} else if (currentValue === 'true' || currentValue === 'false') {
|
| 725 |
+
// This is a boolean
|
| 726 |
+
result[currentKey] = currentValue === 'true';
|
| 727 |
+
} else {
|
| 728 |
+
// This is a string (remove quotes if present)
|
| 729 |
+
result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
// Reset for next key-value pair
|
| 733 |
+
currentKey = '';
|
| 734 |
+
currentValue = '';
|
| 735 |
+
readingKey = true;
|
| 736 |
+
continue;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
// Accumulate characters
|
| 740 |
+
if (readingKey) {
|
| 741 |
+
currentKey += char;
|
| 742 |
+
} else {
|
| 743 |
+
currentValue += char;
|
| 744 |
+
}
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
// Handle the last key-value pair
|
| 748 |
+
if (currentKey) {
|
| 749 |
+
currentKey = currentKey.trim();
|
| 750 |
+
|
| 751 |
+
// If there's no value, this is a flag/marker (like ELSE)
|
| 752 |
+
if (!currentValue) {
|
| 753 |
+
result[currentKey] = true; // Mark it as present
|
| 754 |
+
} else {
|
| 755 |
+
currentValue = currentValue.trim();
|
| 756 |
+
|
| 757 |
+
// Parse the value
|
| 758 |
+
if (currentValue.match(/^\w+\s*\(inputs\(/)) {
|
| 759 |
+
// This is a nested block
|
| 760 |
+
result[currentKey] = currentValue;
|
| 761 |
+
} else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
|
| 762 |
+
// This is a number
|
| 763 |
+
result[currentKey] = parseFloat(currentValue);
|
| 764 |
+
} else if (currentValue === 'true' || currentValue === 'false') {
|
| 765 |
+
// This is a boolean
|
| 766 |
+
result[currentKey] = currentValue === 'true';
|
| 767 |
+
} else {
|
| 768 |
+
// This is a string (remove quotes if present)
|
| 769 |
+
result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
|
| 770 |
+
}
|
| 771 |
+
}
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
return result;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
// Set up unified SSE connection for all workspace operations
|
| 778 |
const setupUnifiedStream = () => {
|
| 779 |
const eventSource = new EventSource('/unified_stream');
|
|
|
|
| 796 |
requestKey = `variable_${data.request_id}`;
|
| 797 |
} else if (data.type === 'edit_mcp') {
|
| 798 |
requestKey = `edit_mcp_${data.request_id}`;
|
| 799 |
+
} else if (data.type === 'replace') {
|
| 800 |
+
requestKey = `replace_${data.request_id}`;
|
| 801 |
}
|
| 802 |
|
| 803 |
// Skip if we've already processed this request
|
|
|
|
| 894 |
console.error('[SSE] Error sending edit MCP result:', err);
|
| 895 |
});
|
| 896 |
}
|
| 897 |
+
// Handle replace block requests
|
| 898 |
+
else if (data.type === 'replace' && data.block_id && data.block_spec && data.request_id) {
|
| 899 |
+
console.log('[SSE] Received replace request for block:', data.block_id, data.block_spec);
|
| 900 |
+
|
| 901 |
+
let success = false;
|
| 902 |
+
let error = null;
|
| 903 |
+
let blockId = null;
|
| 904 |
+
|
| 905 |
+
try {
|
| 906 |
+
// Get the block to be replaced
|
| 907 |
+
const blockToReplace = ws.getBlockById(data.block_id);
|
| 908 |
+
|
| 909 |
+
if (!blockToReplace) {
|
| 910 |
+
throw new Error(`Block ${data.block_id} not found`);
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
// Store the parent and connection info before deletion
|
| 914 |
+
const parentBlock = blockToReplace.getParent();
|
| 915 |
+
const previousBlock = blockToReplace.getPreviousBlock();
|
| 916 |
+
const nextBlock = blockToReplace.getNextBlock();
|
| 917 |
+
let parentConnection = null;
|
| 918 |
+
let connectionType = null;
|
| 919 |
+
let inputName = null;
|
| 920 |
+
|
| 921 |
+
// Check if this block is connected to a parent's input
|
| 922 |
+
if (blockToReplace.outputConnection && blockToReplace.outputConnection.targetConnection) {
|
| 923 |
+
parentConnection = blockToReplace.outputConnection.targetConnection;
|
| 924 |
+
} else if (blockToReplace.previousConnection && blockToReplace.previousConnection.targetConnection) {
|
| 925 |
+
parentConnection = blockToReplace.previousConnection.targetConnection;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
// If the block is in an input socket, get that info
|
| 929 |
+
if (parentBlock) {
|
| 930 |
+
const inputs = parentBlock.inputList;
|
| 931 |
+
for (const input of inputs) {
|
| 932 |
+
if (input.connection && input.connection.targetBlock() === blockToReplace) {
|
| 933 |
+
inputName = input.name;
|
| 934 |
+
break;
|
| 935 |
+
}
|
| 936 |
+
}
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
// Create the new block using the shared function (no positioning, no placement type for replace)
|
| 940 |
+
const newBlock = parseAndCreateBlock(data.block_spec, false, null, null);
|
| 941 |
+
|
| 942 |
+
if (!newBlock) {
|
| 943 |
+
throw new Error('Failed to create replacement block');
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
// Reattach the new block to the parent connection
|
| 947 |
+
if (parentConnection) {
|
| 948 |
+
if (newBlock.outputConnection) {
|
| 949 |
+
parentConnection.connect(newBlock.outputConnection);
|
| 950 |
+
} else if (newBlock.previousConnection) {
|
| 951 |
+
parentConnection.connect(newBlock.previousConnection);
|
| 952 |
+
}
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
// Reattach next block if it was connected
|
| 956 |
+
if (nextBlock && newBlock.nextConnection) {
|
| 957 |
+
newBlock.nextConnection.connect(nextBlock.previousConnection);
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
// Dispose the old block
|
| 961 |
+
blockToReplace.dispose(true);
|
| 962 |
+
|
| 963 |
+
// Render the workspace
|
| 964 |
+
ws.render();
|
| 965 |
+
|
| 966 |
+
success = true;
|
| 967 |
+
blockId = newBlock.id;
|
| 968 |
+
console.log('[SSE] Successfully replaced block:', data.block_id, 'with:', newBlock.id);
|
| 969 |
+
} catch (e) {
|
| 970 |
+
error = e.toString();
|
| 971 |
+
console.error('[SSE] Error replacing block:', e);
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
// Send result back to backend
|
| 975 |
+
console.log('[SSE] Sending replace block result:', { request_id: data.request_id, success, error, block_id: blockId });
|
| 976 |
+
fetch('/replace_block_result', {
|
| 977 |
+
method: 'POST',
|
| 978 |
+
headers: { 'Content-Type': 'application/json' },
|
| 979 |
+
body: JSON.stringify({
|
| 980 |
+
request_id: data.request_id,
|
| 981 |
+
success: success,
|
| 982 |
+
error: error,
|
| 983 |
+
block_id: blockId
|
| 984 |
+
})
|
| 985 |
+
}).then(response => {
|
| 986 |
+
console.log('[SSE] Replace block result sent successfully');
|
| 987 |
+
}).catch(err => {
|
| 988 |
+
console.error('[SSE] Error sending replace block result:', err);
|
| 989 |
+
});
|
| 990 |
+
}
|
| 991 |
// Handle deletion requests
|
| 992 |
else if (data.type === 'delete' && data.block_id) {
|
| 993 |
console.log('[SSE] Received deletion request for block:', data.block_id);
|
|
|
|
| 1043 |
let blockId = null;
|
| 1044 |
|
| 1045 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
// Create the block and all its nested children
|
| 1047 |
const newBlock = parseAndCreateBlock(data.block_spec, true, data.placement_type, data.blockID);
|
| 1048 |
|