Spaces:
Running
Running
owenkaplinsky
commited on
Commit
·
009a2d4
1
Parent(s):
e53c90b
Add more blocks; blocks inside blocks
Browse files- README.md +7 -1
- project/blocks.txt +24 -2
- project/chat.py +37 -5
- project/src/generators/chat.js +2 -2
- project/src/generators/python.js +25 -0
- project/src/index.js +196 -33
README.md
CHANGED
|
@@ -10,7 +10,13 @@ The interface has three main areas. The canvas on the left is where you build by
|
|
| 10 |
|
| 11 |
In the Development tab, you see your generated Python code alongside a test interface. The interface automatically creates input fields matching your tool's parameters. After you arrange your blocks, click Refresh to update the test interface, enter values, and click Submit to run your code. Results appear in the output fields.
|
| 12 |
|
| 13 |
-
The AI Chat tab is the
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
The File menu handles creating new projects, opening existing ones, and downloading your work. You can download just the generated Python code or the entire project as a JSON file. The Edit menu provides standard undo, redo, and a cleanup button to reorganize blocks. The Examples menu includes pre-built projects you can load to understand how to structure your own. You need to set your OpenAI API key through Settings before the system can execute language model calls or power the chat.
|
| 16 |
|
|
|
|
| 10 |
|
| 11 |
In the Development tab, you see your generated Python code alongside a test interface. The interface automatically creates input fields matching your tool's parameters. After you arrange your blocks, click Refresh to update the test interface, enter values, and click Submit to run your code. Results appear in the output fields.
|
| 12 |
|
| 13 |
+
The AI Chat tab is the centerpiece of Agent-Blockly. It connects you with an AI assistant that fully understands your current block configuration and helps you build, modify, and debug your workflows through natural language. You can ask it to add input validation, fix a broken API call, reorganize your logic, or explain how your blocks work.
|
| 14 |
+
|
| 15 |
+
The assistant doesn’t just answer questions - it actively collaborates with you. It can edit your workspace in real time, run your tool with sample inputs, and show you how it behaves. It’s aware of your project’s structure and can make precise, context-aware changes as you iterate.
|
| 16 |
+
|
| 17 |
+
The AI is capable of complex manipulations, such as inserting blocks inside others, creating nested structures, and arranging deeply recursive workflows.
|
| 18 |
+
|
| 19 |
+
While it can handle a wide range of tasks, multi-step or highly complex changes work best when broken into smaller requests. Taking things one step at a time leads to more accurate and reliable results.
|
| 20 |
|
| 21 |
The File menu handles creating new projects, opening existing ones, and downloading your work. You can download just the generated Python code or the entire project as a JSON file. The Edit menu provides standard undo, redo, and a cleanup button to reorganize blocks. The Examples menu includes pre-built projects you can load to understand how to structure your own. You need to set your OpenAI API key through Settings before the system can execute language model calls or power the chat.
|
| 22 |
|
project/blocks.txt
CHANGED
|
@@ -3,24 +3,46 @@ llm_call(inputs(MODEL: "gpt-3.5-turbo-0125/gpt-4o-2024-08-06/gpt-5-mini-2025-08-
|
|
| 3 |
|
| 4 |
# JSON and API
|
| 5 |
in_json(inputs(NAME: value, JSON: value)) // Name is the value you want to extract from the JSON
|
|
|
|
|
|
|
| 6 |
|
| 7 |
# Logic
|
|
|
|
| 8 |
logic_negate(inputs())
|
| 9 |
logic_boolean(inputs(BOOL: "TRUE/FALSE"))
|
| 10 |
logic_null(inputs())
|
| 11 |
logic_compare(inputs(OP: "EQ/NEQ/LT/LTE/GT/GTE", A: value, B: value))
|
| 12 |
logic_operation(inputs(OP: "AND/OR", A: value, B: value))
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
# Math
|
| 15 |
math_number(inputs(NUM: value))
|
| 16 |
math_arithmetic(inputs(OP: "ADD/MINUS/MULTIPLY/DIVIDE/POWER", A: value, B: value)) // A and B are required. They must be blocks, so you need to add math_number or an alternative
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# Text
|
| 19 |
text(inputs(TEXT: value))
|
| 20 |
text_length(VALUE: value)
|
| 21 |
text_join(inputs(ADDN: value)) // N starts at 0; you can make as many N as you want
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# Lists
|
| 24 |
lists_length(inputs(VALUE: value))
|
| 25 |
-
lists_isEmpty(inputs(VALUE: value))
|
| 26 |
-
lists_reverse(inputs(LIST: value))
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
# JSON and API
|
| 5 |
in_json(inputs(NAME: value, JSON: value)) // Name is the value you want to extract from the JSON
|
| 6 |
+
make_json(inputs(KEYN: value, FIELDN: value)) // N starts at 0; you can make as many N as you want. Each key goes with one field
|
| 7 |
+
call_api(inputs(METHOD: "GET/POST/PUT/DELETE", URL: value, HEADERS: value)) // Headers is optional
|
| 8 |
|
| 9 |
# Logic
|
| 10 |
+
controls_if(inputs(IFN: value)) // N starts at 0; you can make as many N as you want
|
| 11 |
logic_negate(inputs())
|
| 12 |
logic_boolean(inputs(BOOL: "TRUE/FALSE"))
|
| 13 |
logic_null(inputs())
|
| 14 |
logic_compare(inputs(OP: "EQ/NEQ/LT/LTE/GT/GTE", A: value, B: value))
|
| 15 |
logic_operation(inputs(OP: "AND/OR", A: value, B: value))
|
| 16 |
|
| 17 |
+
# Loops
|
| 18 |
+
controls_repeat_ext(inputs(TIMES: value))
|
| 19 |
+
controls_whileUntil(inputs(MODE: "WHILE/UNTIL", BOOL: value))
|
| 20 |
+
controls_flow_statements(inputs(FLOW: "CONTINUE/BREAK")) // Must go **inside** of a loop
|
| 21 |
+
|
| 22 |
# Math
|
| 23 |
math_number(inputs(NUM: value))
|
| 24 |
math_arithmetic(inputs(OP: "ADD/MINUS/MULTIPLY/DIVIDE/POWER", A: value, B: value)) // A and B are required. They must be blocks, so you need to add math_number or an alternative
|
| 25 |
+
math_single(inputs(OP: "ROOT/ABS/NEG/LN/LOG10/EXP/POW10", NUM: value)) // EXP is e^num
|
| 26 |
+
math_number_property(inputs(PROPERTY: "EVEN/ODD/PRIME/WHOLE/POSITIVE/NEGATIVE", NUMBER_TO_CHECK: value)) // Boolean output
|
| 27 |
+
math_round(inputs(OP: "ROUND/ROUNDUP/ROUNDDOWN", NUM: value))
|
| 28 |
+
math_modulo(inputs(DIVIDEND: value, DIVISOR: value))
|
| 29 |
+
math_constrain(VALUE: value, LOW: value, HIGH: value)
|
| 30 |
+
math_random_int(inputs(FROM: value, TO: value))
|
| 31 |
|
| 32 |
# Text
|
| 33 |
text(inputs(TEXT: value))
|
| 34 |
text_length(VALUE: value)
|
| 35 |
text_join(inputs(ADDN: value)) // N starts at 0; you can make as many N as you want
|
| 36 |
+
text_isEmpty(inputs(VALUE: value)) // Boolean output
|
| 37 |
+
text_changeCase(inputs(CASE: "UPPERCASE/LOWERCASE/TITLECASE", TEXT: value))
|
| 38 |
+
text_reverse(inputs(TEXT: value))
|
| 39 |
+
text_count(inputs(SUB: value, TEXT: value)) // Integer output
|
| 40 |
+
text_replace(inputs(FROM: value, TO: value, TEXT: value))
|
| 41 |
+
text_trim(inputs(MODE: "BOTH/LEFT/RIGHT", TEXT: value)) // Trim spaces from text
|
| 42 |
|
| 43 |
# Lists
|
| 44 |
lists_length(inputs(VALUE: value))
|
| 45 |
+
lists_isEmpty(inputs(VALUE: value)) // Boolean output
|
| 46 |
+
lists_reverse(inputs(LIST: value))
|
| 47 |
+
lists_create_with(inputs(ADDN: value)) // N starts at 0; you can make as many N as you want
|
| 48 |
+
lists_sort(inputs(TYPE: "NUMERIC/TEXT/IGNORE_CASE", DIRECTION: "1/-1")) // For direction, 1 is ascending, -1 is descending
|
project/chat.py
CHANGED
|
@@ -227,10 +227,12 @@ def delete_block(block_id):
|
|
| 227 |
traceback.print_exc()
|
| 228 |
return f"Error deleting block: {str(e)}"
|
| 229 |
|
| 230 |
-
def create_block(block_spec):
|
| 231 |
"""Create a block in the Blockly workspace"""
|
| 232 |
try:
|
| 233 |
print(f"[CREATE REQUEST] Attempting to create block: {block_spec}")
|
|
|
|
|
|
|
| 234 |
|
| 235 |
# Generate a unique request ID
|
| 236 |
import uuid
|
|
@@ -240,8 +242,11 @@ def create_block(block_spec):
|
|
| 240 |
if request_id in creation_results:
|
| 241 |
creation_results.pop(request_id)
|
| 242 |
|
| 243 |
-
# Add to creation queue
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
| 245 |
print(f"[CREATE REQUEST] Added to queue with ID: {request_id}")
|
| 246 |
|
| 247 |
# Wait for result with timeout
|
|
@@ -485,6 +490,28 @@ List of blocks:
|
|
| 485 |
{blocks_context}
|
| 486 |
|
| 487 |
YOU CANNOT CREATE A MCP BLOCK OR EDIT ITS INPUTS. YOU MUST TELL THE USER TO DO SO.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
"""
|
| 489 |
|
| 490 |
tools = [
|
|
@@ -517,6 +544,10 @@ YOU CANNOT CREATE A MCP BLOCK OR EDIT ITS INPUTS. YOU MUST TELL THE USER TO DO S
|
|
| 517 |
"type": "string",
|
| 518 |
"description": "The create block command using the custom DSL format.",
|
| 519 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
},
|
| 521 |
"required": ["command"],
|
| 522 |
},
|
|
@@ -630,7 +661,8 @@ YOU CANNOT CREATE A MCP BLOCK OR EDIT ITS INPUTS. YOU MUST TELL THE USER TO DO S
|
|
| 630 |
|
| 631 |
elif function_name == "create_block":
|
| 632 |
command = function_args.get("command", "")
|
| 633 |
-
|
|
|
|
| 634 |
result_label = "Create Operation"
|
| 635 |
|
| 636 |
elif function_name == "run_mcp":
|
|
@@ -657,7 +689,7 @@ YOU CANNOT CREATE A MCP BLOCK OR EDIT ITS INPUTS. YOU MUST TELL THE USER TO DO S
|
|
| 657 |
# Update history with the tool call and result
|
| 658 |
temp_history.append({"role": "user", "content": current_prompt})
|
| 659 |
temp_history.append({"role": "assistant", "content": ai_response, "tool_calls": response_message.tool_calls})
|
| 660 |
-
temp_history.append({"role": "tool", "tool_call_id": tool_call.id, "content": tool_result})
|
| 661 |
|
| 662 |
# Set up next prompt to have the model respond to the tool result
|
| 663 |
current_prompt = f"The tool has been executed with the result shown above. Please respond appropriately to the user based on this result."
|
|
|
|
| 227 |
traceback.print_exc()
|
| 228 |
return f"Error deleting block: {str(e)}"
|
| 229 |
|
| 230 |
+
def create_block(block_spec, under_block_id=None):
|
| 231 |
"""Create a block in the Blockly workspace"""
|
| 232 |
try:
|
| 233 |
print(f"[CREATE REQUEST] Attempting to create block: {block_spec}")
|
| 234 |
+
if under_block_id:
|
| 235 |
+
print(f"[CREATE REQUEST] Under block ID: {under_block_id}")
|
| 236 |
|
| 237 |
# Generate a unique request ID
|
| 238 |
import uuid
|
|
|
|
| 242 |
if request_id in creation_results:
|
| 243 |
creation_results.pop(request_id)
|
| 244 |
|
| 245 |
+
# Add to creation queue with optional under_block_id
|
| 246 |
+
queue_data = {"request_id": request_id, "block_spec": block_spec}
|
| 247 |
+
if under_block_id:
|
| 248 |
+
queue_data["under_block_id"] = under_block_id
|
| 249 |
+
creation_queue.put(queue_data)
|
| 250 |
print(f"[CREATE REQUEST] Added to queue with ID: {request_id}")
|
| 251 |
|
| 252 |
# Wait for result with timeout
|
|
|
|
| 490 |
{blocks_context}
|
| 491 |
|
| 492 |
YOU CANNOT CREATE A MCP BLOCK OR EDIT ITS INPUTS. YOU MUST TELL THE USER TO DO SO.
|
| 493 |
+
|
| 494 |
+
For creating blocks, keep in mind: for `value`, if you want to put a string, you need to
|
| 495 |
+
put a string block inside of it - you can't just put the string. For example:
|
| 496 |
+
|
| 497 |
+
text_isEmpty(inputs(VALUE: "text"))
|
| 498 |
+
|
| 499 |
+
This wouldn't work - you can't put a raw string in, you need to create a text block
|
| 500 |
+
recursively inside of this. Same thing for numbers and so on.
|
| 501 |
+
|
| 502 |
+
To put a block inside another (like an if inside a repeat), use two create block commands:
|
| 503 |
+
first for the outer block, then for the inner block under it. This only applies to
|
| 504 |
+
non-outputting blocks; outputting blocks (like a number in an addition) can be nested
|
| 505 |
+
in one command.
|
| 506 |
+
|
| 507 |
+
For blocks that allow infinite things (like ...N) you do not need to provide any inputs
|
| 508 |
+
if you want it to be blank.
|
| 509 |
+
|
| 510 |
+
---
|
| 511 |
+
|
| 512 |
+
Don't share the DSL (DSL being the custom Blockly language) info with the user such
|
| 513 |
+
as when creating a block unless they ask about it. It's not sensitive so you can
|
| 514 |
+
share it with the user if they ask.
|
| 515 |
"""
|
| 516 |
|
| 517 |
tools = [
|
|
|
|
| 544 |
"type": "string",
|
| 545 |
"description": "The create block command using the custom DSL format.",
|
| 546 |
},
|
| 547 |
+
"under": {
|
| 548 |
+
"type": "string",
|
| 549 |
+
"description": "The ID of the block that you want to place this under.",
|
| 550 |
+
},
|
| 551 |
},
|
| 552 |
"required": ["command"],
|
| 553 |
},
|
|
|
|
| 661 |
|
| 662 |
elif function_name == "create_block":
|
| 663 |
command = function_args.get("command", "")
|
| 664 |
+
under_block_id = function_args.get("under", None)
|
| 665 |
+
tool_result = create_block(command, under_block_id)
|
| 666 |
result_label = "Create Operation"
|
| 667 |
|
| 668 |
elif function_name == "run_mcp":
|
|
|
|
| 689 |
# Update history with the tool call and result
|
| 690 |
temp_history.append({"role": "user", "content": current_prompt})
|
| 691 |
temp_history.append({"role": "assistant", "content": ai_response, "tool_calls": response_message.tool_calls})
|
| 692 |
+
temp_history.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(tool_result)})
|
| 693 |
|
| 694 |
# Set up next prompt to have the model respond to the tool result
|
| 695 |
current_prompt = f"The tool has been executed with the result shown above. Please respond appropriately to the user based on this result."
|
project/src/generators/chat.js
CHANGED
|
@@ -247,7 +247,7 @@ chatGenerator.blockToCode = function(block, opt_thisOnly) {
|
|
| 247 |
// Then get all value inputs (connected blocks)
|
| 248 |
const inputList = block.inputList || [];
|
| 249 |
for (const input of inputList) {
|
| 250 |
-
if (input.type === Blockly.
|
| 251 |
const inputName = input.name;
|
| 252 |
const inputValue = this.valueToCode(block, inputName, this.ORDER_ATOMIC);
|
| 253 |
|
|
@@ -263,7 +263,7 @@ chatGenerator.blockToCode = function(block, opt_thisOnly) {
|
|
| 263 |
// Handle statement inputs (for blocks that have a body)
|
| 264 |
let statements = '';
|
| 265 |
for (const input of inputList) {
|
| 266 |
-
if (input.type === Blockly.
|
| 267 |
const statementCode = this.statementToCode(block, input.name);
|
| 268 |
if (statementCode) {
|
| 269 |
statements += statementCode;
|
|
|
|
| 247 |
// Then get all value inputs (connected blocks)
|
| 248 |
const inputList = block.inputList || [];
|
| 249 |
for (const input of inputList) {
|
| 250 |
+
if (input.type === Blockly.INPUT_VALUE && input.connection) {
|
| 251 |
const inputName = input.name;
|
| 252 |
const inputValue = this.valueToCode(block, inputName, this.ORDER_ATOMIC);
|
| 253 |
|
|
|
|
| 263 |
// Handle statement inputs (for blocks that have a body)
|
| 264 |
let statements = '';
|
| 265 |
for (const input of inputList) {
|
| 266 |
+
if (input.type === Blockly.NEXT_STATEMENT && input.connection) {
|
| 267 |
const statementCode = this.statementToCode(block, input.name);
|
| 268 |
if (statementCode) {
|
| 269 |
statements += statementCode;
|
project/src/generators/python.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
| 1 |
import { Order } from 'blockly/python';
|
|
|
|
| 2 |
|
| 3 |
export const forBlock = Object.create(null);
|
| 4 |
|
| 5 |
forBlock['create_mcp'] = function (block, generator) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
const typedInputs = [];
|
| 7 |
let i = 0;
|
| 8 |
|
|
@@ -138,6 +151,18 @@ demo.launch(mcp_server=True)
|
|
| 138 |
};
|
| 139 |
|
| 140 |
forBlock['func_def'] = function (block, generator) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
const name = block.getFieldValue('NAME');
|
| 142 |
const typedInputs = [];
|
| 143 |
let i = 0;
|
|
|
|
| 1 |
import { Order } from 'blockly/python';
|
| 2 |
+
import * as Blockly from 'blockly';
|
| 3 |
|
| 4 |
export const forBlock = Object.create(null);
|
| 5 |
|
| 6 |
forBlock['create_mcp'] = function (block, generator) {
|
| 7 |
+
// Ensure the generator is properly initialized for nested blocks like loops
|
| 8 |
+
if (!generator.nameDB_) {
|
| 9 |
+
generator.nameDB_ = new Blockly.Names(generator.RESERVED_WORDS_ || []);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
// Ensure getDistinctName is available for control flow blocks
|
| 13 |
+
if (!generator.getDistinctName) {
|
| 14 |
+
generator.getDistinctName = function(name, type) {
|
| 15 |
+
return this.nameDB_.getDistinctName(name, type);
|
| 16 |
+
};
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
const typedInputs = [];
|
| 20 |
let i = 0;
|
| 21 |
|
|
|
|
| 151 |
};
|
| 152 |
|
| 153 |
forBlock['func_def'] = function (block, generator) {
|
| 154 |
+
// Ensure the generator is properly initialized for nested blocks like loops
|
| 155 |
+
if (!generator.nameDB_) {
|
| 156 |
+
generator.nameDB_ = new Blockly.Names(generator.RESERVED_WORDS_ || []);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// Ensure getDistinctName is available for control flow blocks
|
| 160 |
+
if (!generator.getDistinctName) {
|
| 161 |
+
generator.getDistinctName = function(name, type) {
|
| 162 |
+
return this.nameDB_.getDistinctName(name, type);
|
| 163 |
+
};
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
const name = block.getFieldValue('NAME');
|
| 167 |
const typedInputs = [];
|
| 168 |
let i = 0;
|
project/src/index.js
CHANGED
|
@@ -9,6 +9,37 @@ import '@blockly/toolbox-search';
|
|
| 9 |
import DarkTheme from '@blockly/theme-dark';
|
| 10 |
import './index.css';
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
// Register the blocks and generator with Blockly
|
| 13 |
Blockly.common.defineBlocks(blocks);
|
| 14 |
Object.assign(pythonGenerator.forBlock, forBlock);
|
|
@@ -364,44 +395,107 @@ const setupCreationStream = () => {
|
|
| 364 |
const inputs = parseInputs(inputsContent);
|
| 365 |
console.log('[SSE CREATE] Parsed inputs:', inputs);
|
| 366 |
|
| 367 |
-
//
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
-
|
| 376 |
-
const input = newBlock.getInput(key);
|
| 377 |
-
if (input && input.connection) {
|
| 378 |
-
childBlock.outputConnection.connect(input.connection);
|
| 379 |
-
}
|
| 380 |
-
} else {
|
| 381 |
-
// This is a simple value, set it as a field
|
| 382 |
-
// Remove quotes if present
|
| 383 |
-
const cleanValue = value.replace(/^["']|["']$/g, '');
|
| 384 |
|
| 385 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
try {
|
| 387 |
-
newBlock.setFieldValue(
|
| 388 |
} catch (e) {
|
| 389 |
-
console.log(`[SSE CREATE] Could not set field ${key} to ${
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
}
|
| 391 |
-
}
|
| 392 |
-
} else if (typeof value === 'number') {
|
| 393 |
-
// Set numeric field value
|
| 394 |
-
try {
|
| 395 |
-
newBlock.setFieldValue(value, key);
|
| 396 |
-
} catch (e) {
|
| 397 |
-
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 398 |
-
}
|
| 399 |
-
} else if (typeof value === 'boolean') {
|
| 400 |
-
// Set boolean field value
|
| 401 |
-
try {
|
| 402 |
-
newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
|
| 403 |
-
} catch (e) {
|
| 404 |
-
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 405 |
}
|
| 406 |
}
|
| 407 |
}
|
|
@@ -534,6 +628,71 @@ const setupCreationStream = () => {
|
|
| 534 |
|
| 535 |
if (newBlock) {
|
| 536 |
blockId = newBlock.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
success = true;
|
| 538 |
console.log('[SSE CREATE] Successfully created block with children:', blockId, newBlock.type);
|
| 539 |
} else {
|
|
@@ -597,6 +756,10 @@ const observer = new ResizeObserver(() => {
|
|
| 597 |
observer.observe(blocklyDiv);
|
| 598 |
|
| 599 |
const updateCode = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
// Instead of using workspaceToCode which processes ALL blocks,
|
| 601 |
// manually process only blocks connected to create_mcp or func_def
|
| 602 |
let code = '';
|
|
|
|
| 9 |
import DarkTheme from '@blockly/theme-dark';
|
| 10 |
import './index.css';
|
| 11 |
|
| 12 |
+
// Initialize the Python generator's Names database if it doesn't exist
|
| 13 |
+
if (!pythonGenerator.nameDB_) {
|
| 14 |
+
pythonGenerator.init = function(workspace) {
|
| 15 |
+
// Call parent init if it exists
|
| 16 |
+
if (Blockly.Generator.prototype.init) {
|
| 17 |
+
Blockly.Generator.prototype.init.call(this, workspace);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Initialize the Names database for variable name generation
|
| 21 |
+
if (!this.nameDB_) {
|
| 22 |
+
this.nameDB_ = new Blockly.Names(this.RESERVED_WORDS_);
|
| 23 |
+
} else {
|
| 24 |
+
this.nameDB_.reset();
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Add reserved Python keywords
|
| 28 |
+
const reservedWords = ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
|
| 29 |
+
'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
|
| 30 |
+
'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
|
| 31 |
+
'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return',
|
| 32 |
+
'try', 'while', 'with', 'yield'];
|
| 33 |
+
|
| 34 |
+
for (const word of reservedWords) {
|
| 35 |
+
this.nameDB_.setVariableMap(word, word);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Initialize function name database
|
| 39 |
+
this.functionNames_ = this.nameDB_;
|
| 40 |
+
};
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
// Register the blocks and generator with Blockly
|
| 44 |
Blockly.common.defineBlocks(blocks);
|
| 45 |
Object.assign(pythonGenerator.forBlock, forBlock);
|
|
|
|
| 395 |
const inputs = parseInputs(inputsContent);
|
| 396 |
console.log('[SSE CREATE] Parsed inputs:', inputs);
|
| 397 |
|
| 398 |
+
// Special handling for make_json block
|
| 399 |
+
if (blockType === 'make_json') {
|
| 400 |
+
// Count FIELD entries to determine how many fields we need
|
| 401 |
+
let fieldCount = 0;
|
| 402 |
+
const fieldValues = {};
|
| 403 |
+
const keyValues = {};
|
| 404 |
+
|
| 405 |
+
for (const [key, value] of Object.entries(inputs)) {
|
| 406 |
+
const fieldMatch = key.match(/^FIELD(\d+)$/);
|
| 407 |
+
const keyMatch = key.match(/^KEY(\d+)$/);
|
| 408 |
+
|
| 409 |
+
if (fieldMatch) {
|
| 410 |
+
const index = parseInt(fieldMatch[1]);
|
| 411 |
+
fieldCount = Math.max(fieldCount, index + 1);
|
| 412 |
+
fieldValues[index] = value;
|
| 413 |
+
} else if (keyMatch) {
|
| 414 |
+
const index = parseInt(keyMatch[1]);
|
| 415 |
+
keyValues[index] = value;
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
// Set up the mutator state
|
| 420 |
+
if (fieldCount > 0) {
|
| 421 |
+
newBlock.fieldCount_ = fieldCount;
|
| 422 |
+
newBlock.fieldKeys_ = [];
|
| 423 |
+
|
| 424 |
+
// Create the inputs through the mutator
|
| 425 |
+
for (let i = 0; i < fieldCount; i++) {
|
| 426 |
+
const keyValue = keyValues[i];
|
| 427 |
+
const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
|
| 428 |
+
? keyValue.replace(/^["']|["']$/g, '')
|
| 429 |
+
: `key${i}`;
|
| 430 |
|
| 431 |
+
newBlock.fieldKeys_[i] = key;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
|
| 433 |
+
// Create the input
|
| 434 |
+
const input = newBlock.appendValueInput('FIELD' + i);
|
| 435 |
+
const field = new Blockly.FieldTextInput(key);
|
| 436 |
+
field.setValidator((newValue) => {
|
| 437 |
+
newBlock.fieldKeys_[i] = newValue || `key${i}`;
|
| 438 |
+
return newValue;
|
| 439 |
+
});
|
| 440 |
+
input.appendField(field, 'KEY' + i);
|
| 441 |
+
input.appendField(':');
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
// Now connect the field values
|
| 445 |
+
for (let i = 0; i < fieldCount; i++) {
|
| 446 |
+
const value = fieldValues[i];
|
| 447 |
+
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
|
| 448 |
+
// This is a nested block, create it recursively
|
| 449 |
+
const childBlock = parseAndCreateBlock(value);
|
| 450 |
+
|
| 451 |
+
// Connect the child block to the FIELD input
|
| 452 |
+
const input = newBlock.getInput('FIELD' + i);
|
| 453 |
+
if (input && input.connection && childBlock.outputConnection) {
|
| 454 |
+
childBlock.outputConnection.connect(input.connection);
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
} else {
|
| 460 |
+
// Normal block handling
|
| 461 |
+
for (const [key, value] of Object.entries(inputs)) {
|
| 462 |
+
if (typeof value === 'string') {
|
| 463 |
+
// Check if this is a nested block specification
|
| 464 |
+
if (value.match(/^\w+\s*\(inputs\(/)) {
|
| 465 |
+
// This is a nested block, create it recursively
|
| 466 |
+
const childBlock = parseAndCreateBlock(value);
|
| 467 |
+
|
| 468 |
+
// Connect the child block to the appropriate input
|
| 469 |
+
const input = newBlock.getInput(key);
|
| 470 |
+
if (input && input.connection && childBlock.outputConnection) {
|
| 471 |
+
childBlock.outputConnection.connect(input.connection);
|
| 472 |
+
}
|
| 473 |
+
} else {
|
| 474 |
+
// This is a simple value, set it as a field
|
| 475 |
+
// Remove quotes if present
|
| 476 |
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
| 477 |
+
|
| 478 |
+
// Try to set as a field value
|
| 479 |
+
try {
|
| 480 |
+
newBlock.setFieldValue(cleanValue, key);
|
| 481 |
+
} catch (e) {
|
| 482 |
+
console.log(`[SSE CREATE] Could not set field ${key} to ${cleanValue}:`, e);
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
} else if (typeof value === 'number') {
|
| 486 |
+
// Set numeric field value
|
| 487 |
try {
|
| 488 |
+
newBlock.setFieldValue(value, key);
|
| 489 |
} catch (e) {
|
| 490 |
+
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 491 |
+
}
|
| 492 |
+
} else if (typeof value === 'boolean') {
|
| 493 |
+
// Set boolean field value
|
| 494 |
+
try {
|
| 495 |
+
newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
|
| 496 |
+
} catch (e) {
|
| 497 |
+
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
|
| 498 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
}
|
| 500 |
}
|
| 501 |
}
|
|
|
|
| 628 |
|
| 629 |
if (newBlock) {
|
| 630 |
blockId = newBlock.id;
|
| 631 |
+
|
| 632 |
+
// If under_block_id is specified, attach the new block under the parent
|
| 633 |
+
if (data.under_block_id) {
|
| 634 |
+
const parentBlock = ws.getBlockById(data.under_block_id);
|
| 635 |
+
if (parentBlock) {
|
| 636 |
+
console.log('[SSE CREATE] Attaching to parent block:', data.under_block_id);
|
| 637 |
+
|
| 638 |
+
// Find an appropriate input to connect to
|
| 639 |
+
// Try common statement inputs first
|
| 640 |
+
const statementInputs = ['BODY', 'DO', 'THEN', 'ELSE', 'STACK'];
|
| 641 |
+
let connected = false;
|
| 642 |
+
|
| 643 |
+
for (const inputName of statementInputs) {
|
| 644 |
+
const input = parentBlock.getInput(inputName);
|
| 645 |
+
if (input && input.type === Blockly.NEXT_STATEMENT) {
|
| 646 |
+
// Check if something is already connected
|
| 647 |
+
if (input.connection && !input.connection.targetBlock()) {
|
| 648 |
+
// Connect directly
|
| 649 |
+
if (newBlock.previousConnection) {
|
| 650 |
+
input.connection.connect(newBlock.previousConnection);
|
| 651 |
+
connected = true;
|
| 652 |
+
console.log('[SSE CREATE] Connected to input:', inputName);
|
| 653 |
+
break;
|
| 654 |
+
}
|
| 655 |
+
} else if (input.connection && input.connection.targetBlock()) {
|
| 656 |
+
// Find the last block in the stack
|
| 657 |
+
let lastBlock = input.connection.targetBlock();
|
| 658 |
+
while (lastBlock.nextConnection && lastBlock.nextConnection.targetBlock()) {
|
| 659 |
+
lastBlock = lastBlock.nextConnection.targetBlock();
|
| 660 |
+
}
|
| 661 |
+
// Connect to the end of the stack
|
| 662 |
+
if (lastBlock.nextConnection && newBlock.previousConnection) {
|
| 663 |
+
lastBlock.nextConnection.connect(newBlock.previousConnection);
|
| 664 |
+
connected = true;
|
| 665 |
+
console.log('[SSE CREATE] Connected to end of stack in input:', inputName);
|
| 666 |
+
break;
|
| 667 |
+
}
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// If not connected to statement input, try value inputs
|
| 673 |
+
if (!connected) {
|
| 674 |
+
// Try all inputs
|
| 675 |
+
const inputs = parentBlock.inputList;
|
| 676 |
+
for (const input of inputs) {
|
| 677 |
+
if (input.type === Blockly.INPUT_VALUE && input.connection && !input.connection.targetBlock()) {
|
| 678 |
+
if (newBlock.outputConnection) {
|
| 679 |
+
input.connection.connect(newBlock.outputConnection);
|
| 680 |
+
connected = true;
|
| 681 |
+
console.log('[SSE CREATE] Connected to value input:', input.name);
|
| 682 |
+
break;
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
if (!connected) {
|
| 689 |
+
console.warn('[SSE CREATE] Could not find suitable connection point on parent block');
|
| 690 |
+
}
|
| 691 |
+
} else {
|
| 692 |
+
console.warn('[SSE CREATE] Parent block not found:', data.under_block_id);
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
success = true;
|
| 697 |
console.log('[SSE CREATE] Successfully created block with children:', blockId, newBlock.type);
|
| 698 |
} else {
|
|
|
|
| 756 |
observer.observe(blocklyDiv);
|
| 757 |
|
| 758 |
const updateCode = () => {
|
| 759 |
+
// Initialize the Python generator with the workspace before generating code
|
| 760 |
+
// This ensures the Names database is properly set up for control flow blocks
|
| 761 |
+
pythonGenerator.init(ws);
|
| 762 |
+
|
| 763 |
// Instead of using workspaceToCode which processes ALL blocks,
|
| 764 |
// manually process only blocks connected to create_mcp or func_def
|
| 765 |
let code = '';
|