Spaces:
Running
Running
| import * as Blockly from 'blockly'; | |
| // Create a custom generator for the chat/AI language | |
| class ChatGenerator extends Blockly.Generator { | |
| constructor(name) { | |
| super(name); | |
| // Custom order definitions for the chat language if needed | |
| this.ORDER_ATOMIC = 0; | |
| } | |
| // Override scrub_ to handle how blocks are joined | |
| scrub_(block, code, thisOnly) { | |
| const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); | |
| let nextCode = ''; | |
| if (nextBlock) { | |
| nextCode = this.blockToCode(nextBlock); | |
| } | |
| return code + nextCode; | |
| } | |
| } | |
| // Create an instance of the chat generator | |
| export const chatGenerator = new ChatGenerator('Chat'); | |
| // Export forBlock for custom block definitions | |
| export const forBlock = Object.create(null); | |
| /* | |
| This file is for the secondary code generator. | |
| It is not meant to be shown to the user, but rather to communicate the state of the workspace to an AI Assistant assistant in a simplistic text format. | |
| */ | |
| forBlock['create_mcp'] = function (block, generator) { | |
| const inputParams = []; | |
| const outputParams = []; | |
| let i = 0; | |
| // Build list of input parameters with types | |
| while (block.getInput('X' + i)) { | |
| const input = block.getInput('X' + i); | |
| if (input && input.connection && input.connection.targetBlock()) { | |
| const paramName = (block.inputNames_ && block.inputNames_[i]) || ('arg' + i); | |
| const type = (block.inputTypes_ && block.inputTypes_[i]) || 'string'; | |
| inputParams.push(`${paramName}: ${type}`); | |
| } | |
| i++; | |
| } | |
| // Build list of output parameters with values (not types) | |
| if (block.outputCount_ && block.outputCount_ > 0 && block.getInput('R0')) { | |
| for (let r = 0; r < block.outputCount_; r++) { | |
| const outputName = (block.outputNames_ && block.outputNames_[r]) || `result${r}`; | |
| let returnValue = generator.valueToCode(block, 'R' + r, chatGenerator.ORDER_ATOMIC) || 'None'; | |
| // Replace placeholder args with actual names | |
| if (returnValue && block.inputNames_) { | |
| for (let j = 0; j < block.inputNames_.length; j++) { | |
| const paramName = block.inputNames_[j]; | |
| returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName); | |
| } | |
| } | |
| outputParams.push(`${outputName}: ${returnValue}`); | |
| } | |
| } | |
| // Main function body | |
| let body = generator.statementToCode(block, 'BODY'); | |
| // Construct the create_mcp call with inputs and outputs | |
| // Include block ID for deletion tracking with arrow separator | |
| let code = `${block.id} → create_mcp(inputs(${inputParams.join(', ')}), outputs(${outputParams.join(', ')}))` | |
| // Add the function body | |
| if (body) { | |
| code += body; | |
| } | |
| return code; | |
| }; | |
| forBlock['func_def'] = function (block, generator) { | |
| const name = block.getFieldValue('NAME'); | |
| const inputParams = []; | |
| const outputParams = []; | |
| let i = 0; | |
| // Build list of input parameters with types | |
| while (block.getInput('X' + i)) { | |
| const input = block.getInput('X' + i); | |
| if (input && input.connection && input.connection.targetBlock()) { | |
| const paramName = (block.inputNames_ && block.inputNames_[i]) || ('arg' + i); | |
| const type = (block.inputTypes_ && block.inputTypes_[i]) || 'string'; | |
| inputParams.push(`${paramName}: ${type}`); | |
| } | |
| i++; | |
| } | |
| // Build list of output parameters with values (not types) | |
| if (block.outputCount_ && block.outputCount_ > 0 && block.getInput('R0')) { | |
| for (let r = 0; r < block.outputCount_; r++) { | |
| const outputName = (block.outputNames_ && block.outputNames_[r]) || `output${r}`; | |
| let returnValue = generator.valueToCode(block, 'R' + r, chatGenerator.ORDER_ATOMIC) || 'None'; | |
| // Replace placeholder args with actual names | |
| if (returnValue && block.inputNames_) { | |
| for (let j = 0; j < block.inputNames_.length; j++) { | |
| const paramName = block.inputNames_[j]; | |
| returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName); | |
| } | |
| } | |
| outputParams.push(`${outputName}: ${returnValue}`); | |
| } | |
| } | |
| // Main function body | |
| let body = generator.statementToCode(block, 'BODY'); | |
| // Construct the func_def call with inputs and outputs | |
| // Include block ID for deletion tracking with arrow separator | |
| let code = `${block.id} → ${name}(inputs(${inputParams.join(', ')}), outputs(${outputParams.join(', ')}))` | |
| // Add the function body | |
| if (body) { | |
| code += body; | |
| } | |
| return code; | |
| }; | |
| // Handler for input reference blocks | |
| forBlock['input_reference'] = function (block, generator) { | |
| const varName = block.getFieldValue('VARNAME') || | |
| block.type.replace('input_reference_', '') || | |
| 'unnamed_arg'; | |
| // Value blocks must return a tuple: [code, order] | |
| return [varName, chatGenerator.ORDER_ATOMIC]; | |
| }; | |
| // Register the forBlock definitions with the chat generator | |
| Object.assign(chatGenerator.forBlock, forBlock); | |
| // Override workspaceToCode to include standalone value blocks | |
| chatGenerator.workspaceToCode = function (workspace) { | |
| if (!workspace) { | |
| // Backwards compatibility from before there could be multiple workspaces. | |
| console.warn('No workspace specified in workspaceToCode call. Guessing.'); | |
| workspace = Blockly.getMainWorkspace(); | |
| } | |
| let code = []; | |
| const blocks = workspace.getTopBlocks(true); | |
| for (let i = 0; i < blocks.length; i++) { | |
| const block = blocks[i]; | |
| // Process ALL top-level blocks, including value blocks | |
| if (block.outputConnection) { | |
| // This is a value block - check if it's connected | |
| if (!block.outputConnection.isConnected()) { | |
| // Standalone value block - get its code | |
| const line = this.blockToCode(block, true); | |
| if (Array.isArray(line)) { | |
| // Value blocks return [code, order], extract just the code | |
| const blockCode = line[0]; | |
| if (blockCode) { | |
| code.push(blockCode); | |
| } | |
| } else if (line) { | |
| code.push(line); | |
| } | |
| } | |
| } else { | |
| // Regular statement block | |
| const line = this.blockToCode(block); | |
| if (Array.isArray(line)) { | |
| // Shouldn't happen for statement blocks, but handle it anyway | |
| code.push(line[0]); | |
| } else if (line) { | |
| code.push(line); | |
| } | |
| } | |
| } | |
| code = code.join('\n'); // Blank line between each section. | |
| // Strip trailing whitespace | |
| code = code.replace(/\n\s*$/g, '\n'); | |
| return code; | |
| }; | |
| // Override blockToCode to provide a catch-all handler | |
| const originalBlockToCode = chatGenerator.blockToCode.bind(chatGenerator); | |
| chatGenerator.blockToCode = function (block, opt_thisOnly) { | |
| // Null check | |
| if (!block) { | |
| return ''; | |
| } | |
| // Check if it's an input reference block type | |
| if (block.type.startsWith('input_reference_')) { | |
| const varName = block.getFieldValue('VARNAME') || | |
| block.type.replace('input_reference_', '') || | |
| 'unnamed_arg'; | |
| // Value blocks must return a tuple: [code, order] | |
| return [varName, this.ORDER_ATOMIC]; | |
| } | |
| // Try the normal generation first | |
| try { | |
| return originalBlockToCode(block, opt_thisOnly); | |
| } catch (e) { | |
| // Catch-all handler for blocks without specific generators | |
| const blockType = block.type; | |
| const inputs = []; | |
| // Special handling for common blocks with field values | |
| if (blockType === 'text') { | |
| const text = block.getFieldValue('TEXT'); | |
| if (text !== null && text !== undefined) { | |
| inputs.push(`TEXT: "${text}"`); | |
| } | |
| } else if (blockType === 'math_number') { | |
| const num = block.getFieldValue('NUM'); | |
| if (num !== null && num !== undefined) { | |
| inputs.push(`NUM: ${num}`); | |
| } | |
| } else if (blockType === 'controls_if') { | |
| // Special handling for if/else blocks | |
| // Extract all condition values in the proper format: IF, IFELSEN0, IFELSEN1, etc. | |
| const ifCount = (block.inputList.filter(input => input.name && input.name.match(/^IF\d+$/)).length) || 1; | |
| // Get the first IF condition | |
| const if0Input = block.getInput('IF0'); | |
| if (if0Input && if0Input.connection) { | |
| const condValue = this.valueToCode(block, 'IF0', this.ORDER_ATOMIC); | |
| if (condValue) { | |
| inputs.push(`IF: ${condValue}`); | |
| } | |
| } | |
| // Get all additional IF conditions (IF1, IF2, etc.) as IFELSEN0, IFELSEN1, etc. | |
| for (let i = 1; i < ifCount; i++) { | |
| const ifInput = block.getInput('IF' + i); | |
| if (ifInput && ifInput.connection) { | |
| const condValue = this.valueToCode(block, 'IF' + i, this.ORDER_ATOMIC); | |
| if (condValue) { | |
| inputs.push(`IFELSEN${i - 1}: ${condValue}`); | |
| } | |
| } | |
| } | |
| // Check if ELSE exists (look for DO blocks and see if there's an ELSE) | |
| const hasElse = block.getInput('ELSE') !== null && block.getInput('ELSE') !== undefined; | |
| // If ELSE exists, add it to inputs (it's just a marker, no condition value) | |
| if (hasElse) { | |
| inputs.push(`ELSE`); | |
| } | |
| // Generate the controls_if call with conditions | |
| let code = `${block.id} → ${blockType}(inputs(${inputs.join(', ')}))`; | |
| // Now get all the statement blocks with proper formatting | |
| // DO0, DO1, etc. are indented under the if | |
| // ELSE blocks are labeled with "Else:" prefix | |
| for (let i = 0; i < ifCount; i++) { | |
| const doInput = block.getInput('DO' + i); | |
| if (doInput) { | |
| const doCode = this.statementToCode(block, 'DO' + i); | |
| if (doCode) { | |
| // Indent each line of the statement code | |
| const indentedCode = doCode.split('\n').map(line => line ? ' ' + line : '').join('\n'); | |
| code += '\n' + indentedCode; | |
| } | |
| } | |
| } | |
| // Get ELSE block if it exists - format it with "Else:" label | |
| if (hasElse) { | |
| const elseCode = this.statementToCode(block, 'ELSE'); | |
| if (elseCode) { | |
| code += '\nElse:\n'; | |
| // Indent each line of the else code | |
| const indentedCode = elseCode.split('\n').map(line => line ? ' ' + line : '').join('\n'); | |
| code += indentedCode; | |
| } | |
| } | |
| // Handle the next block in the sequence for statement chaining | |
| if (!opt_thisOnly) { | |
| const nextCode = this.scrub_(block, code, opt_thisOnly); | |
| return nextCode; | |
| } | |
| return code + '\n'; | |
| } else { | |
| // Generic field value extraction for other blocks | |
| // Get all inputs to check for fields | |
| const inputList = block.inputList || []; | |
| for (const input of inputList) { | |
| // Check fields in each input | |
| if (input.fieldRow) { | |
| for (const field of input.fieldRow) { | |
| if (field && field.name && field.getValue) { | |
| const value = field.getValue(); | |
| if (value !== null && value !== undefined && value !== '') { | |
| // Format the value appropriately | |
| const formattedValue = typeof value === 'string' ? `"${value}"` : value; | |
| inputs.push(`${field.name}: ${formattedValue}`); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Then get all value inputs (connected blocks) | |
| const inputList = block.inputList || []; | |
| for (const input of inputList) { | |
| if (input.type === Blockly.INPUT_VALUE && input.connection) { | |
| const inputName = input.name; | |
| const inputValue = this.valueToCode(block, inputName, this.ORDER_ATOMIC); | |
| if (inputValue) { | |
| inputs.push(`${inputName}: ${inputValue}`); | |
| } | |
| } | |
| } | |
| // Generate the standard format: name(inputs(...)) with block ID and arrow separator | |
| const code = `${block.id} → ${blockType}(inputs(${inputs.join(', ')}))`; | |
| // Handle statement inputs (for blocks that have a body) | |
| let statements = ''; | |
| for (const input of inputList) { | |
| if (input.type === Blockly.NEXT_STATEMENT && input.connection) { | |
| const statementCode = this.statementToCode(block, input.name); | |
| if (statementCode) { | |
| // Indent statement code (4 spaces) if this block will be a statement block | |
| if (!block.outputConnection) { | |
| // Only indent for statement blocks; value blocks handle their own formatting | |
| const indentedCode = statementCode.split('\n').map(line => line ? ' ' + line : '').join('\n'); | |
| statements += indentedCode; | |
| } else { | |
| statements += statementCode; | |
| } | |
| } | |
| } | |
| } | |
| // Return appropriate format based on whether it's a value or statement block | |
| if (block.outputConnection) { | |
| // This is a value block (can be plugged into inputs) | |
| // Check if this block is connected to another block's input | |
| const isConnectedToInput = block.outputConnection && block.outputConnection.isConnected(); | |
| if (isConnectedToInput) { | |
| // When used as input to another block, don't include the ID | |
| const valueCode = `${blockType}(inputs(${inputs.join(', ')}))`; | |
| return [valueCode, this.ORDER_ATOMIC]; | |
| } else { | |
| // When standalone (not connected), include the ID | |
| const standaloneCode = `${block.id} → ${blockType}(inputs(${inputs.join(', ')}))`; | |
| // For standalone value blocks, we need to return them as statement-like | |
| // but still maintain the value block return format for Blockly | |
| return [standaloneCode, this.ORDER_ATOMIC]; | |
| } | |
| } else { | |
| // This is a statement block (has prev/next connections) | |
| const fullCode = code + (statements ? '\n' + statements : ''); | |
| // Handle the next block in the sequence if not opt_thisOnly | |
| if (!opt_thisOnly) { | |
| const nextCode = this.scrub_(block, fullCode, opt_thisOnly); | |
| return nextCode; | |
| } | |
| return fullCode + '\n'; | |
| } | |
| } | |
| }; |