owenkaplinsky commited on
Commit
ab1ed73
·
1 Parent(s): c8dc711

Fix variable errors

Browse files
project/src/generators/chat.js CHANGED
@@ -53,7 +53,7 @@ forBlock['create_mcp'] = function (block, generator) {
53
  for (let r = 0; r < block.outputCount_; r++) {
54
  const outputName = (block.outputNames_ && block.outputNames_[r]) || `result${r}`;
55
  let returnValue = generator.valueToCode(block, 'R' + r, chatGenerator.ORDER_ATOMIC) || 'None';
56
-
57
  // Replace placeholder args with actual names
58
  if (returnValue && block.inputNames_) {
59
  for (let j = 0; j < block.inputNames_.length; j++) {
@@ -61,7 +61,7 @@ forBlock['create_mcp'] = function (block, generator) {
61
  returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName);
62
  }
63
  }
64
-
65
  outputParams.push(`${outputName}: ${returnValue}`);
66
  }
67
  }
@@ -103,7 +103,7 @@ forBlock['func_def'] = function (block, generator) {
103
  for (let r = 0; r < block.outputCount_; r++) {
104
  const outputName = (block.outputNames_ && block.outputNames_[r]) || `output${r}`;
105
  let returnValue = generator.valueToCode(block, 'R' + r, chatGenerator.ORDER_ATOMIC) || 'None';
106
-
107
  // Replace placeholder args with actual names
108
  if (returnValue && block.inputNames_) {
109
  for (let j = 0; j < block.inputNames_.length; j++) {
@@ -111,7 +111,7 @@ forBlock['func_def'] = function (block, generator) {
111
  returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName);
112
  }
113
  }
114
-
115
  outputParams.push(`${outputName}: ${returnValue}`);
116
  }
117
  }
@@ -132,10 +132,10 @@ forBlock['func_def'] = function (block, generator) {
132
  };
133
 
134
  // Handler for input reference blocks
135
- forBlock['input_reference'] = function(block, generator) {
136
- const varName = block.getFieldValue('VARNAME') ||
137
- block.type.replace('input_reference_', '') ||
138
- 'unnamed_arg';
139
  // Value blocks must return a tuple: [code, order]
140
  return [varName, chatGenerator.ORDER_ATOMIC];
141
  };
@@ -144,7 +144,7 @@ forBlock['input_reference'] = function(block, generator) {
144
  Object.assign(chatGenerator.forBlock, forBlock);
145
 
146
  // Override workspaceToCode to include standalone value blocks
147
- chatGenerator.workspaceToCode = function(workspace) {
148
  if (!workspace) {
149
  // Backwards compatibility from before there could be multiple workspaces.
150
  console.warn('No workspace specified in workspaceToCode call. Guessing.');
@@ -189,21 +189,21 @@ chatGenerator.workspaceToCode = function(workspace) {
189
 
190
  // Override blockToCode to provide a catch-all handler
191
  const originalBlockToCode = chatGenerator.blockToCode.bind(chatGenerator);
192
- chatGenerator.blockToCode = function(block, opt_thisOnly) {
193
  // Null check
194
  if (!block) {
195
  return '';
196
  }
197
-
198
  // Check if it's an input reference block type
199
  if (block.type.startsWith('input_reference_')) {
200
- const varName = block.getFieldValue('VARNAME') ||
201
- block.type.replace('input_reference_', '') ||
202
- 'unnamed_arg';
203
  // Value blocks must return a tuple: [code, order]
204
  return [varName, this.ORDER_ATOMIC];
205
  }
206
-
207
  // Try the normal generation first
208
  try {
209
  return originalBlockToCode(block, opt_thisOnly);
@@ -211,7 +211,7 @@ chatGenerator.blockToCode = function(block, opt_thisOnly) {
211
  // Catch-all handler for blocks without specific generators
212
  const blockType = block.type;
213
  const inputs = [];
214
-
215
  // Special handling for common blocks with field values
216
  if (blockType === 'text') {
217
  const text = block.getFieldValue('TEXT');
@@ -243,23 +243,23 @@ chatGenerator.blockToCode = function(block, opt_thisOnly) {
243
  }
244
  }
245
  }
246
-
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
-
254
  if (inputValue) {
255
  inputs.push(`${inputName}: ${inputValue}`);
256
  }
257
  }
258
  }
259
-
260
  // Generate the standard format: name(inputs(...)) with block ID and pipe separator
261
  const code = `${block.id} | ${blockType}(inputs(${inputs.join(', ')}))`;
262
-
263
  // Handle statement inputs (for blocks that have a body)
264
  let statements = '';
265
  for (const input of inputList) {
@@ -270,13 +270,13 @@ chatGenerator.blockToCode = function(block, opt_thisOnly) {
270
  }
271
  }
272
  }
273
-
274
  // Return appropriate format based on whether it's a value or statement block
275
  if (block.outputConnection) {
276
  // This is a value block (can be plugged into inputs)
277
  // Check if this block is connected to another block's input
278
  const isConnectedToInput = block.outputConnection && block.outputConnection.isConnected();
279
-
280
  if (isConnectedToInput) {
281
  // When used as input to another block, don't include the ID
282
  const valueCode = `${blockType}(inputs(${inputs.join(', ')}))`;
@@ -291,13 +291,13 @@ chatGenerator.blockToCode = function(block, opt_thisOnly) {
291
  } else {
292
  // This is a statement block (has prev/next connections)
293
  const fullCode = code + (statements ? '\n' + statements : '');
294
-
295
  // Handle the next block in the sequence if not opt_thisOnly
296
  if (!opt_thisOnly) {
297
  const nextCode = this.scrub_(block, fullCode, opt_thisOnly);
298
  return nextCode;
299
  }
300
-
301
  return fullCode + '\n';
302
  }
303
  }
 
53
  for (let r = 0; r < block.outputCount_; r++) {
54
  const outputName = (block.outputNames_ && block.outputNames_[r]) || `result${r}`;
55
  let returnValue = generator.valueToCode(block, 'R' + r, chatGenerator.ORDER_ATOMIC) || 'None';
56
+
57
  // Replace placeholder args with actual names
58
  if (returnValue && block.inputNames_) {
59
  for (let j = 0; j < block.inputNames_.length; j++) {
 
61
  returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName);
62
  }
63
  }
64
+
65
  outputParams.push(`${outputName}: ${returnValue}`);
66
  }
67
  }
 
103
  for (let r = 0; r < block.outputCount_; r++) {
104
  const outputName = (block.outputNames_ && block.outputNames_[r]) || `output${r}`;
105
  let returnValue = generator.valueToCode(block, 'R' + r, chatGenerator.ORDER_ATOMIC) || 'None';
106
+
107
  // Replace placeholder args with actual names
108
  if (returnValue && block.inputNames_) {
109
  for (let j = 0; j < block.inputNames_.length; j++) {
 
111
  returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName);
112
  }
113
  }
114
+
115
  outputParams.push(`${outputName}: ${returnValue}`);
116
  }
117
  }
 
132
  };
133
 
134
  // Handler for input reference blocks
135
+ forBlock['input_reference'] = function (block, generator) {
136
+ const varName = block.getFieldValue('VARNAME') ||
137
+ block.type.replace('input_reference_', '') ||
138
+ 'unnamed_arg';
139
  // Value blocks must return a tuple: [code, order]
140
  return [varName, chatGenerator.ORDER_ATOMIC];
141
  };
 
144
  Object.assign(chatGenerator.forBlock, forBlock);
145
 
146
  // Override workspaceToCode to include standalone value blocks
147
+ chatGenerator.workspaceToCode = function (workspace) {
148
  if (!workspace) {
149
  // Backwards compatibility from before there could be multiple workspaces.
150
  console.warn('No workspace specified in workspaceToCode call. Guessing.');
 
189
 
190
  // Override blockToCode to provide a catch-all handler
191
  const originalBlockToCode = chatGenerator.blockToCode.bind(chatGenerator);
192
+ chatGenerator.blockToCode = function (block, opt_thisOnly) {
193
  // Null check
194
  if (!block) {
195
  return '';
196
  }
197
+
198
  // Check if it's an input reference block type
199
  if (block.type.startsWith('input_reference_')) {
200
+ const varName = block.getFieldValue('VARNAME') ||
201
+ block.type.replace('input_reference_', '') ||
202
+ 'unnamed_arg';
203
  // Value blocks must return a tuple: [code, order]
204
  return [varName, this.ORDER_ATOMIC];
205
  }
206
+
207
  // Try the normal generation first
208
  try {
209
  return originalBlockToCode(block, opt_thisOnly);
 
211
  // Catch-all handler for blocks without specific generators
212
  const blockType = block.type;
213
  const inputs = [];
214
+
215
  // Special handling for common blocks with field values
216
  if (blockType === 'text') {
217
  const text = block.getFieldValue('TEXT');
 
243
  }
244
  }
245
  }
246
+
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
+
254
  if (inputValue) {
255
  inputs.push(`${inputName}: ${inputValue}`);
256
  }
257
  }
258
  }
259
+
260
  // Generate the standard format: name(inputs(...)) with block ID and pipe separator
261
  const code = `${block.id} | ${blockType}(inputs(${inputs.join(', ')}))`;
262
+
263
  // Handle statement inputs (for blocks that have a body)
264
  let statements = '';
265
  for (const input of inputList) {
 
270
  }
271
  }
272
  }
273
+
274
  // Return appropriate format based on whether it's a value or statement block
275
  if (block.outputConnection) {
276
  // This is a value block (can be plugged into inputs)
277
  // Check if this block is connected to another block's input
278
  const isConnectedToInput = block.outputConnection && block.outputConnection.isConnected();
279
+
280
  if (isConnectedToInput) {
281
  // When used as input to another block, don't include the ID
282
  const valueCode = `${blockType}(inputs(${inputs.join(', ')}))`;
 
291
  } else {
292
  // This is a statement block (has prev/next connections)
293
  const fullCode = code + (statements ? '\n' + statements : '');
294
+
295
  // Handle the next block in the sequence if not opt_thisOnly
296
  if (!opt_thisOnly) {
297
  const nextCode = this.scrub_(block, fullCode, opt_thisOnly);
298
  return nextCode;
299
  }
300
+
301
  return fullCode + '\n';
302
  }
303
  }
project/src/generators/python.js CHANGED
@@ -8,14 +8,14 @@ forBlock['create_mcp'] = function (block, generator) {
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
 
@@ -155,14 +155,14 @@ forBlock['func_def'] = function (block, generator) {
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;
 
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
 
 
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.html CHANGED
@@ -3,13 +3,13 @@
3
 
4
  <head>
5
  <meta charset="utf-8" />
6
- <title>Blockly MCP Builder</title>
7
  </head>
8
 
9
  <body>
10
  <div id="topBar">
11
  <div id="titleSection">
12
- <h1>Blockly MCP Builder</h1>
13
  </div>
14
  <div id="divider"></div>
15
  <div id="menuSection">
@@ -193,22 +193,34 @@
193
  </script>
194
 
195
  <!-- Keys Modal -->
196
- <div id="apiKeyModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; align-items: center; justify-content: center;">
197
- <div style="background: white; padding: 30px; border-radius: 10px; width: 90%; max-width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);">
 
 
198
  <h2 style="margin-top: 0; margin-bottom: 20px; color: #333;">API Keys</h2>
199
-
200
- <label for="apiKeyInput" style="display: block; margin-bottom: 10px; color: #666; font-size: 14px; font-weight: 500;">OpenAI API Key:</label>
201
- <input type="password" id="apiKeyInput" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; box-sizing: border-box; margin-bottom: 5px;" placeholder="sk-...">
 
 
 
 
202
  <p style="margin: 5px 0 15px 0; color: #999; font-size: 12px;">For the AI assistant and blocks' model calls.</p>
203
-
204
- <label for="hfKeyInput" style="display: block; margin-bottom: 10px; color: #666; font-size: 14px; font-weight: 500;">Hugging Face API Key:</label>
205
- <input type="password" id="hfKeyInput" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; box-sizing: border-box; margin-bottom: 5px;" placeholder="hf_...">
 
 
 
 
206
  <p style="margin: 5px 0 20px 0; color: #999; font-size: 12px;">For deploying your MCP server.</p>
207
-
208
  <p style="color: #999; font-size: 12px;">Your API keys will be stored securely for this session.</p>
209
  <div style="margin-top: 20px; display: flex; justify-content: flex-end; gap: 10px;">
210
- <button id="cancelApiKey" style="padding: 10px 20px; background: #e5e7eb; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;">Cancel</button>
211
- <button id="saveApiKey" style="padding: 10px 20px; background: #6366f1; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;">Save</button>
 
 
212
  </div>
213
  </div>
214
  </div>
 
3
 
4
  <head>
5
  <meta charset="utf-8" />
6
+ <title>MCP Blockly</title>
7
  </head>
8
 
9
  <body>
10
  <div id="topBar">
11
  <div id="titleSection">
12
+ <h1>MCP Blockly</h1>
13
  </div>
14
  <div id="divider"></div>
15
  <div id="menuSection">
 
193
  </script>
194
 
195
  <!-- Keys Modal -->
196
+ <div id="apiKeyModal"
197
+ style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; align-items: center; justify-content: center;">
198
+ <div
199
+ style="background: white; padding: 30px; border-radius: 10px; width: 90%; max-width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);">
200
  <h2 style="margin-top: 0; margin-bottom: 20px; color: #333;">API Keys</h2>
201
+
202
+ <label for="apiKeyInput"
203
+ style="display: block; margin-bottom: 10px; color: #666; font-size: 14px; font-weight: 500;">OpenAI API
204
+ Key:</label>
205
+ <input type="password" id="apiKeyInput"
206
+ style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; box-sizing: border-box; margin-bottom: 5px;"
207
+ placeholder="sk-...">
208
  <p style="margin: 5px 0 15px 0; color: #999; font-size: 12px;">For the AI assistant and blocks' model calls.</p>
209
+
210
+ <label for="hfKeyInput"
211
+ style="display: block; margin-bottom: 10px; color: #666; font-size: 14px; font-weight: 500;">Hugging Face API
212
+ Key:</label>
213
+ <input type="password" id="hfKeyInput"
214
+ style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; box-sizing: border-box; margin-bottom: 5px;"
215
+ placeholder="hf_...">
216
  <p style="margin: 5px 0 20px 0; color: #999; font-size: 12px;">For deploying your MCP server.</p>
217
+
218
  <p style="color: #999; font-size: 12px;">Your API keys will be stored securely for this session.</p>
219
  <div style="margin-top: 20px; display: flex; justify-content: flex-end; gap: 10px;">
220
+ <button id="cancelApiKey"
221
+ style="padding: 10px 20px; background: #e5e7eb; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;">Cancel</button>
222
+ <button id="saveApiKey"
223
+ style="padding: 10px 20px; background: #6366f1; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;">Save</button>
224
  </div>
225
  </div>
226
  </div>
project/src/index.js CHANGED
@@ -9,37 +9,6 @@ import '@blockly/toolbox-search';
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);
@@ -132,7 +101,7 @@ downloadCodeButton.addEventListener("click", () => {
132
  // Get the current generated code
133
  const codeEl = document.querySelector('#generatedCode code');
134
  const code = codeEl ? codeEl.textContent : '';
135
-
136
  if (!code) {
137
  alert('No code to download');
138
  return;
@@ -140,7 +109,7 @@ downloadCodeButton.addEventListener("click", () => {
140
 
141
  var filename = "app.py";
142
  var element = document.createElement('a');
143
-
144
  element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(code));
145
  element.setAttribute('download', filename);
146
  element.style.display = 'none';
@@ -159,19 +128,19 @@ const cancelApiKeyButton = document.querySelector('#cancelApiKey');
159
 
160
  settingsButton.addEventListener("click", () => {
161
  apiKeyModal.style.display = 'flex';
162
-
163
  // Load current API keys from backend
164
  fetch("/get_api_key", {
165
  method: "GET",
166
  })
167
- .then(response => response.json())
168
- .then(data => {
169
- apiKeyInput.value = data.api_key || '';
170
- hfKeyInput.value = data.hf_key || '';
171
- })
172
- .catch(err => {
173
- console.error("Error loading API keys:", err);
174
- });
175
  });
176
 
177
  saveApiKeyButton.addEventListener("click", () => {
@@ -203,19 +172,19 @@ saveApiKeyButton.addEventListener("click", () => {
203
  body: JSON.stringify({ api_key: apiKey, hf_key: hfKey }),
204
  })
205
  ])
206
- .then(async (responses) => {
207
- const results = await Promise.all(responses.map(r => r.json()));
208
- if (results.every(r => r.success)) {
209
- alert('API keys saved successfully');
210
- apiKeyModal.style.display = 'none';
211
- } else {
212
- alert('Failed to save API keys to all services');
213
- }
214
- })
215
- .catch(err => {
216
- console.error("Error saving API keys:", err);
217
- alert('Failed to save API keys');
218
- });
219
  });
220
 
221
  cancelApiKeyButton.addEventListener("click", () => {
@@ -258,14 +227,14 @@ cleanWorkspace.addEventListener("click", () => {
258
  const setupDeletionStream = () => {
259
  const eventSource = new EventSource('/delete_stream');
260
  const processedRequests = new Set(); // Track processed deletion requests
261
-
262
  eventSource.onmessage = (event) => {
263
  try {
264
  const data = JSON.parse(event.data);
265
-
266
  // Skip heartbeat messages
267
  if (data.heartbeat) return;
268
-
269
  // Skip if we've already processed this exact request
270
  const requestKey = `${data.block_id}_${Date.now()}`;
271
  if (data.block_id && processedRequests.has(data.block_id)) {
@@ -277,15 +246,15 @@ const setupDeletionStream = () => {
277
  // Clear after 10 seconds to allow retries if needed
278
  setTimeout(() => processedRequests.delete(data.block_id), 10000);
279
  }
280
-
281
  if (data.block_id) {
282
  console.log('[SSE] Received deletion request for block:', data.block_id);
283
-
284
  // Try to delete the block
285
  const block = ws.getBlockById(data.block_id);
286
  let success = false;
287
  let error = null;
288
-
289
  if (block) {
290
  console.log('[SSE] Found block to delete:', block.type, block.id);
291
  // Check if it's the main create_mcp block (which shouldn't be deleted)
@@ -306,7 +275,7 @@ const setupDeletionStream = () => {
306
  error = 'Block not found';
307
  console.log('[SSE] Block not found:', data.block_id);
308
  }
309
-
310
  // Send result back to backend immediately
311
  console.log('[SSE] Sending deletion result:', { block_id: data.block_id, success, error });
312
  fetch('/deletion_result', {
@@ -327,7 +296,7 @@ const setupDeletionStream = () => {
327
  console.error('[SSE] Error processing message:', err);
328
  }
329
  };
330
-
331
  eventSource.onerror = (error) => {
332
  console.error('[SSE] Connection error:', error);
333
  // Reconnect after 5 seconds
@@ -336,7 +305,7 @@ const setupDeletionStream = () => {
336
  setupDeletionStream();
337
  }, 5000);
338
  };
339
-
340
  eventSource.onopen = () => {
341
  console.log('[SSE] Connected to deletion stream');
342
  };
@@ -349,14 +318,14 @@ setupDeletionStream();
349
  const setupCreationStream = () => {
350
  const eventSource = new EventSource('/create_stream');
351
  const processedRequests = new Set(); // Track processed creation requests
352
-
353
  eventSource.onmessage = (event) => {
354
  try {
355
  const data = JSON.parse(event.data);
356
-
357
  // Skip heartbeat messages
358
  if (data.heartbeat) return;
359
-
360
  // Skip if we've already processed this request
361
  if (data.request_id && processedRequests.has(data.request_id)) {
362
  console.log('[SSE CREATE] Skipping duplicate creation request:', data.request_id);
@@ -367,54 +336,54 @@ const setupCreationStream = () => {
367
  // Clear after 10 seconds to allow retries if needed
368
  setTimeout(() => processedRequests.delete(data.request_id), 10000);
369
  }
370
-
371
  if (data.block_spec && data.request_id) {
372
  console.log('[SSE CREATE] Received creation request:', data.request_id, data.block_spec);
373
-
374
  let success = false;
375
  let error = null;
376
  let blockId = null;
377
-
378
  try {
379
  // Parse and create blocks recursively
380
  function parseAndCreateBlock(spec, shouldPosition = false) {
381
  // Match block_name(inputs(...))
382
  const blockMatch = spec.match(/^(\w+)\s*\((.+)\)$/s);
383
-
384
  if (!blockMatch) {
385
  throw new Error(`Invalid block specification format: ${spec}`);
386
  }
387
-
388
  const blockType = blockMatch[1];
389
  const content = blockMatch[2].trim();
390
-
391
  console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
392
-
393
  // Check if this has inputs() wrapper
394
  let inputsContent = content;
395
  if (content.startsWith('inputs(') && content.endsWith(')')) {
396
  inputsContent = content.slice(7, -1); // Remove 'inputs(' and ')'
397
  }
398
-
399
  // Create the block
400
  const newBlock = ws.newBlock(blockType);
401
-
402
  if (inputsContent) {
403
  // Parse the inputs content
404
  const inputs = parseInputs(inputsContent);
405
  console.log('[SSE CREATE] Parsed inputs:', inputs);
406
-
407
  // Special handling for make_json block
408
  if (blockType === 'make_json') {
409
  // Count FIELD entries to determine how many fields we need
410
  let fieldCount = 0;
411
  const fieldValues = {};
412
  const keyValues = {};
413
-
414
  for (const [key, value] of Object.entries(inputs)) {
415
  const fieldMatch = key.match(/^FIELD(\d+)$/);
416
  const keyMatch = key.match(/^KEY(\d+)$/);
417
-
418
  if (fieldMatch) {
419
  const index = parseInt(fieldMatch[1]);
420
  fieldCount = Math.max(fieldCount, index + 1);
@@ -424,21 +393,21 @@ const setupCreationStream = () => {
424
  keyValues[index] = value;
425
  }
426
  }
427
-
428
  // Set up the mutator state
429
  if (fieldCount > 0) {
430
  newBlock.fieldCount_ = fieldCount;
431
  newBlock.fieldKeys_ = [];
432
-
433
  // Create the inputs through the mutator
434
  for (let i = 0; i < fieldCount; i++) {
435
  const keyValue = keyValues[i];
436
- const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
437
- ? keyValue.replace(/^["']|["']$/g, '')
438
  : `key${i}`;
439
-
440
  newBlock.fieldKeys_[i] = key;
441
-
442
  // Create the input
443
  const input = newBlock.appendValueInput('FIELD' + i);
444
  const field = new Blockly.FieldTextInput(key);
@@ -449,14 +418,14 @@ const setupCreationStream = () => {
449
  input.appendField(field, 'KEY' + i);
450
  input.appendField(':');
451
  }
452
-
453
  // Now connect the field values
454
  for (let i = 0; i < fieldCount; i++) {
455
  const value = fieldValues[i];
456
  if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
457
  // This is a nested block, create it recursively
458
  const childBlock = parseAndCreateBlock(value);
459
-
460
  // Connect the child block to the FIELD input
461
  const input = newBlock.getInput('FIELD' + i);
462
  if (input && input.connection && childBlock.outputConnection) {
@@ -473,7 +442,7 @@ const setupCreationStream = () => {
473
  if (value.match(/^\w+\s*\(inputs\(/)) {
474
  // This is a nested block, create it recursively
475
  const childBlock = parseAndCreateBlock(value);
476
-
477
  // Connect the child block to the appropriate input
478
  const input = newBlock.getInput(key);
479
  if (input && input.connection && childBlock.outputConnection) {
@@ -483,7 +452,7 @@ const setupCreationStream = () => {
483
  // This is a simple value, set it as a field
484
  // Remove quotes if present
485
  const cleanValue = value.replace(/^["']|["']$/g, '');
486
-
487
  // Try to set as a field value
488
  try {
489
  newBlock.setFieldValue(cleanValue, key);
@@ -509,33 +478,33 @@ const setupCreationStream = () => {
509
  }
510
  }
511
  }
512
-
513
  // Initialize the block (renders it)
514
  newBlock.initSvg();
515
-
516
  // Only position the top-level block
517
  if (shouldPosition) {
518
  // Find a good position that doesn't overlap existing blocks
519
  const existingBlocks = ws.getAllBlocks();
520
  let x = 50;
521
  let y = 50;
522
-
523
  // Simple positioning: stack new blocks vertically
524
  if (existingBlocks.length > 0) {
525
  const lastBlock = existingBlocks[existingBlocks.length - 1];
526
  const lastPos = lastBlock.getRelativeToSurfaceXY();
527
  y = lastPos.y + lastBlock.height + 20;
528
  }
529
-
530
  newBlock.moveBy(x, y);
531
  }
532
-
533
  // Render the block
534
  newBlock.render();
535
-
536
  return newBlock;
537
  }
538
-
539
  // Helper function to parse inputs(key: value, key2: value2, ...)
540
  function parseInputs(inputStr) {
541
  const result = {};
@@ -545,12 +514,12 @@ const setupCreationStream = () => {
545
  let inQuotes = false;
546
  let quoteChar = '';
547
  let readingKey = true;
548
-
549
  for (let i = 0; i < inputStr.length; i++) {
550
  const char = inputStr[i];
551
-
552
  // Handle quotes
553
- if ((char === '"' || char === "'") && (i === 0 || inputStr[i-1] !== '\\')) {
554
  if (!inQuotes) {
555
  inQuotes = true;
556
  quoteChar = char;
@@ -559,25 +528,25 @@ const setupCreationStream = () => {
559
  quoteChar = '';
560
  }
561
  }
562
-
563
  // Handle parentheses depth (for nested blocks)
564
  if (!inQuotes) {
565
  if (char === '(') depth++;
566
  else if (char === ')') depth--;
567
  }
568
-
569
  // Handle key-value separation
570
  if (char === ':' && depth === 0 && !inQuotes && readingKey) {
571
  readingKey = false;
572
  currentKey = currentKey.trim();
573
  continue;
574
  }
575
-
576
  // Handle comma separation
577
  if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
578
  // Store the key-value pair
579
  currentValue = currentValue.trim();
580
-
581
  // Parse the value
582
  if (currentValue.match(/^\w+\s*\(inputs\(/)) {
583
  // This is a nested block
@@ -592,14 +561,14 @@ const setupCreationStream = () => {
592
  // This is a string (remove quotes if present)
593
  result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
594
  }
595
-
596
  // Reset for next key-value pair
597
  currentKey = '';
598
  currentValue = '';
599
  readingKey = true;
600
  continue;
601
  }
602
-
603
  // Accumulate characters
604
  if (readingKey) {
605
  currentKey += char;
@@ -607,12 +576,12 @@ const setupCreationStream = () => {
607
  currentValue += char;
608
  }
609
  }
610
-
611
  // Handle the last key-value pair
612
  if (currentKey && currentValue) {
613
  currentKey = currentKey.trim();
614
  currentValue = currentValue.trim();
615
-
616
  // Parse the value
617
  if (currentValue.match(/^\w+\s*\(inputs\(/)) {
618
  // This is a nested block
@@ -628,27 +597,27 @@ const setupCreationStream = () => {
628
  result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
629
  }
630
  }
631
-
632
  return result;
633
  }
634
-
635
  // Create the block and all its nested children
636
  const newBlock = parseAndCreateBlock(data.block_spec, true);
637
-
638
  if (newBlock) {
639
  blockId = newBlock.id;
640
-
641
  // If under_block_id is specified, attach the new block under the parent
642
  if (data.under_block_id) {
643
  const parentBlock = ws.getBlockById(data.under_block_id);
644
  if (parentBlock) {
645
  console.log('[SSE CREATE] Attaching to parent block:', data.under_block_id);
646
-
647
  // Find an appropriate input to connect to
648
  // Try common statement inputs first
649
  const statementInputs = ['BODY', 'DO', 'THEN', 'ELSE', 'STACK'];
650
  let connected = false;
651
-
652
  for (const inputName of statementInputs) {
653
  const input = parentBlock.getInput(inputName);
654
  if (input && input.type === Blockly.NEXT_STATEMENT) {
@@ -677,7 +646,7 @@ const setupCreationStream = () => {
677
  }
678
  }
679
  }
680
-
681
  // If not connected to statement input, try value inputs
682
  if (!connected) {
683
  // Try all inputs
@@ -693,7 +662,7 @@ const setupCreationStream = () => {
693
  }
694
  }
695
  }
696
-
697
  if (!connected) {
698
  console.warn('[SSE CREATE] Could not find suitable connection point on parent block');
699
  }
@@ -701,26 +670,26 @@ const setupCreationStream = () => {
701
  console.warn('[SSE CREATE] Parent block not found:', data.under_block_id);
702
  }
703
  }
704
-
705
  success = true;
706
  console.log('[SSE CREATE] Successfully created block with children:', blockId, newBlock.type);
707
  } else {
708
  throw new Error(`Failed to create block from specification`);
709
  }
710
-
711
  } catch (e) {
712
  error = e.toString();
713
  console.error('[SSE CREATE] Error creating block:', e);
714
  }
715
-
716
  // Send result back to backend immediately
717
- console.log('[SSE CREATE] Sending creation result:', {
718
- request_id: data.request_id,
719
- success,
720
  error,
721
- block_id: blockId
722
  });
723
-
724
  fetch('/creation_result', {
725
  method: 'POST',
726
  headers: { 'Content-Type': 'application/json' },
@@ -740,7 +709,7 @@ const setupCreationStream = () => {
740
  console.error('[SSE CREATE] Error processing message:', err);
741
  }
742
  };
743
-
744
  eventSource.onerror = (error) => {
745
  console.error('[SSE CREATE] Connection error:', error);
746
  // Reconnect after 5 seconds
@@ -749,7 +718,7 @@ const setupCreationStream = () => {
749
  setupCreationStream();
750
  }, 5000);
751
  };
752
-
753
  eventSource.onopen = () => {
754
  console.log('[SSE CREATE] Connected to creation stream');
755
  };
@@ -761,14 +730,14 @@ setupCreationStream();
761
  const setupVariableStream = () => {
762
  const eventSource = new EventSource('/variable_stream');
763
  const processedRequests = new Set(); // Track processed variable requests
764
-
765
  eventSource.onmessage = (event) => {
766
  try {
767
  const data = JSON.parse(event.data);
768
-
769
  // Skip heartbeat messages
770
  if (data.heartbeat) return;
771
-
772
  // Skip if we've already processed this request
773
  if (data.request_id && processedRequests.has(data.request_id)) {
774
  console.log('[SSE VARIABLE] Skipping duplicate variable request:', data.request_id);
@@ -779,21 +748,21 @@ const setupVariableStream = () => {
779
  // Clear after 10 seconds to allow retries if needed
780
  setTimeout(() => processedRequests.delete(data.request_id), 10000);
781
  }
782
-
783
  if (data.variable_name && data.request_id) {
784
  console.log('[SSE VARIABLE] Received variable creation request:', data.request_id, data.variable_name);
785
-
786
  let success = false;
787
  let error = null;
788
  let variableId = null;
789
-
790
  try {
791
  // Create the variable using Blockly's variable map
792
  const variableName = data.variable_name;
793
-
794
  // Use the workspace's variable map to create a new variable
795
  const variableModel = ws.getVariableMap().createVariable(variableName);
796
-
797
  if (variableModel) {
798
  variableId = variableModel.getId();
799
  success = true;
@@ -801,20 +770,20 @@ const setupVariableStream = () => {
801
  } else {
802
  throw new Error('Failed to create variable model');
803
  }
804
-
805
  } catch (e) {
806
  error = e.toString();
807
  console.error('[SSE VARIABLE] Error creating variable:', e);
808
  }
809
-
810
  // Send result back to backend immediately
811
- console.log('[SSE VARIABLE] Sending variable creation result:', {
812
- request_id: data.request_id,
813
- success,
814
  error,
815
- variable_id: variableId
816
  });
817
-
818
  fetch('/variable_result', {
819
  method: 'POST',
820
  headers: { 'Content-Type': 'application/json' },
@@ -834,7 +803,7 @@ const setupVariableStream = () => {
834
  console.error('[SSE VARIABLE] Error processing message:', err);
835
  }
836
  };
837
-
838
  eventSource.onerror = (error) => {
839
  console.error('[SSE VARIABLE] Connection error:', error);
840
  // Reconnect after 5 seconds
@@ -843,7 +812,7 @@ const setupVariableStream = () => {
843
  setupVariableStream();
844
  }, 5000);
845
  };
846
-
847
  eventSource.onopen = () => {
848
  console.log('[SSE VARIABLE] Connected to variable stream');
849
  };
@@ -862,14 +831,14 @@ const updateCode = () => {
862
  // Initialize the Python generator with the workspace before generating code
863
  // This ensures the Names database is properly set up for control flow blocks
864
  pythonGenerator.init(ws);
865
-
866
  // Instead of using workspaceToCode which processes ALL blocks,
867
  // manually process only blocks connected to create_mcp or func_def
868
  let code = '';
869
-
870
  // Get all top-level blocks (not connected to other blocks)
871
  const topBlocks = ws.getTopBlocks(false);
872
-
873
  // Process only create_mcp and func_def blocks
874
  for (const block of topBlocks) {
875
  if (block.type === 'create_mcp' || block.type === 'func_def') {
@@ -888,7 +857,7 @@ const updateCode = () => {
888
 
889
  const vars = ws.getVariableMap().getAllVariables();
890
  globalVarString = vars.map(v => `${v.id} | ${v.name}`).join("\n");
891
-
892
  const codeEl = document.querySelector('#generatedCode code');
893
 
894
  const call = `def llm_call(prompt, model):
@@ -998,12 +967,12 @@ const sendChatUpdate = async (chatCode, retryCount = 0) => {
998
  const response = await fetch("/update_chat", {
999
  method: "POST",
1000
  headers: { "Content-Type": "application/json" },
1001
- body: JSON.stringify({
1002
  code: chatCode,
1003
  varString: globalVarString
1004
  }),
1005
  });
1006
-
1007
  if (response.ok) {
1008
  chatBackendAvailable = true;
1009
  console.log("[Blockly] Sent updated Chat code to backend");
@@ -1013,7 +982,7 @@ const sendChatUpdate = async (chatCode, retryCount = 0) => {
1013
  } catch (err) {
1014
  console.warn(`[Blockly] Chat backend not ready (attempt ${retryCount + 1}):`, err.message);
1015
  chatBackendAvailable = false;
1016
-
1017
  // Queue this update for retry
1018
  if (retryCount < 5) {
1019
  const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
@@ -1039,7 +1008,7 @@ const updateChatCode = () => {
1039
 
1040
  // You can add any chat-specific preprocessing here
1041
  // For example, adding headers or formatting
1042
-
1043
  if (codeEl) {
1044
  codeEl.textContent = globalChatCode;
1045
  }
@@ -1050,15 +1019,15 @@ const updateChatCode = () => {
1050
  } else {
1051
  // Queue the update and try to establish connection
1052
  chatUpdateQueue.push(globalChatCode);
1053
-
1054
  // Clear any existing retry timeout
1055
  if (chatRetryTimeout) {
1056
  clearTimeout(chatRetryTimeout);
1057
  }
1058
-
1059
  // Try to connect to backend
1060
  checkChatBackend();
1061
-
1062
  // Set up periodic retry
1063
  chatRetryTimeout = setTimeout(() => {
1064
  checkChatBackend();
@@ -1080,7 +1049,7 @@ try {
1080
  if (!block.inputRefBlocks_) {
1081
  block.inputRefBlocks_ = new Map();
1082
  }
1083
-
1084
  // Create reference blocks for each input if they don't exist
1085
  if (block.inputNames_ && block.inputNames_.length > 0) {
1086
  for (let i = 0; i < block.inputNames_.length; i++) {
@@ -1090,13 +1059,13 @@ try {
1090
  if (input && input.connection) {
1091
  const connectedBlock = input.connection.targetBlock();
1092
  const expectedType = `input_reference_${name}`;
1093
-
1094
  // If there's already the correct block connected, just track it
1095
  if (connectedBlock && connectedBlock.type === expectedType) {
1096
  connectedBlock._ownerBlockId = block.id;
1097
  connectedBlock.setDeletable(false);
1098
  block.inputRefBlocks_.set(name, connectedBlock);
1099
- }
1100
  // Only create if input exists AND has no connected block yet
1101
  else if (!connectedBlock) {
1102
  // Create the reference block
 
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);
 
101
  // Get the current generated code
102
  const codeEl = document.querySelector('#generatedCode code');
103
  const code = codeEl ? codeEl.textContent : '';
104
+
105
  if (!code) {
106
  alert('No code to download');
107
  return;
 
109
 
110
  var filename = "app.py";
111
  var element = document.createElement('a');
112
+
113
  element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(code));
114
  element.setAttribute('download', filename);
115
  element.style.display = 'none';
 
128
 
129
  settingsButton.addEventListener("click", () => {
130
  apiKeyModal.style.display = 'flex';
131
+
132
  // Load current API keys from backend
133
  fetch("/get_api_key", {
134
  method: "GET",
135
  })
136
+ .then(response => response.json())
137
+ .then(data => {
138
+ apiKeyInput.value = data.api_key || '';
139
+ hfKeyInput.value = data.hf_key || '';
140
+ })
141
+ .catch(err => {
142
+ console.error("Error loading API keys:", err);
143
+ });
144
  });
145
 
146
  saveApiKeyButton.addEventListener("click", () => {
 
172
  body: JSON.stringify({ api_key: apiKey, hf_key: hfKey }),
173
  })
174
  ])
175
+ .then(async (responses) => {
176
+ const results = await Promise.all(responses.map(r => r.json()));
177
+ if (results.every(r => r.success)) {
178
+ alert('API keys saved successfully');
179
+ apiKeyModal.style.display = 'none';
180
+ } else {
181
+ alert('Failed to save API keys to all services');
182
+ }
183
+ })
184
+ .catch(err => {
185
+ console.error("Error saving API keys:", err);
186
+ alert('Failed to save API keys');
187
+ });
188
  });
189
 
190
  cancelApiKeyButton.addEventListener("click", () => {
 
227
  const setupDeletionStream = () => {
228
  const eventSource = new EventSource('/delete_stream');
229
  const processedRequests = new Set(); // Track processed deletion requests
230
+
231
  eventSource.onmessage = (event) => {
232
  try {
233
  const data = JSON.parse(event.data);
234
+
235
  // Skip heartbeat messages
236
  if (data.heartbeat) return;
237
+
238
  // Skip if we've already processed this exact request
239
  const requestKey = `${data.block_id}_${Date.now()}`;
240
  if (data.block_id && processedRequests.has(data.block_id)) {
 
246
  // Clear after 10 seconds to allow retries if needed
247
  setTimeout(() => processedRequests.delete(data.block_id), 10000);
248
  }
249
+
250
  if (data.block_id) {
251
  console.log('[SSE] Received deletion request for block:', data.block_id);
252
+
253
  // Try to delete the block
254
  const block = ws.getBlockById(data.block_id);
255
  let success = false;
256
  let error = null;
257
+
258
  if (block) {
259
  console.log('[SSE] Found block to delete:', block.type, block.id);
260
  // Check if it's the main create_mcp block (which shouldn't be deleted)
 
275
  error = 'Block not found';
276
  console.log('[SSE] Block not found:', data.block_id);
277
  }
278
+
279
  // Send result back to backend immediately
280
  console.log('[SSE] Sending deletion result:', { block_id: data.block_id, success, error });
281
  fetch('/deletion_result', {
 
296
  console.error('[SSE] Error processing message:', err);
297
  }
298
  };
299
+
300
  eventSource.onerror = (error) => {
301
  console.error('[SSE] Connection error:', error);
302
  // Reconnect after 5 seconds
 
305
  setupDeletionStream();
306
  }, 5000);
307
  };
308
+
309
  eventSource.onopen = () => {
310
  console.log('[SSE] Connected to deletion stream');
311
  };
 
318
  const setupCreationStream = () => {
319
  const eventSource = new EventSource('/create_stream');
320
  const processedRequests = new Set(); // Track processed creation requests
321
+
322
  eventSource.onmessage = (event) => {
323
  try {
324
  const data = JSON.parse(event.data);
325
+
326
  // Skip heartbeat messages
327
  if (data.heartbeat) return;
328
+
329
  // Skip if we've already processed this request
330
  if (data.request_id && processedRequests.has(data.request_id)) {
331
  console.log('[SSE CREATE] Skipping duplicate creation request:', data.request_id);
 
336
  // Clear after 10 seconds to allow retries if needed
337
  setTimeout(() => processedRequests.delete(data.request_id), 10000);
338
  }
339
+
340
  if (data.block_spec && data.request_id) {
341
  console.log('[SSE CREATE] Received creation request:', data.request_id, data.block_spec);
342
+
343
  let success = false;
344
  let error = null;
345
  let blockId = null;
346
+
347
  try {
348
  // Parse and create blocks recursively
349
  function parseAndCreateBlock(spec, shouldPosition = false) {
350
  // Match block_name(inputs(...))
351
  const blockMatch = spec.match(/^(\w+)\s*\((.+)\)$/s);
352
+
353
  if (!blockMatch) {
354
  throw new Error(`Invalid block specification format: ${spec}`);
355
  }
356
+
357
  const blockType = blockMatch[1];
358
  const content = blockMatch[2].trim();
359
+
360
  console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
361
+
362
  // Check if this has inputs() wrapper
363
  let inputsContent = content;
364
  if (content.startsWith('inputs(') && content.endsWith(')')) {
365
  inputsContent = content.slice(7, -1); // Remove 'inputs(' and ')'
366
  }
367
+
368
  // Create the block
369
  const newBlock = ws.newBlock(blockType);
370
+
371
  if (inputsContent) {
372
  // Parse the inputs content
373
  const inputs = parseInputs(inputsContent);
374
  console.log('[SSE CREATE] Parsed inputs:', inputs);
375
+
376
  // Special handling for make_json block
377
  if (blockType === 'make_json') {
378
  // Count FIELD entries to determine how many fields we need
379
  let fieldCount = 0;
380
  const fieldValues = {};
381
  const keyValues = {};
382
+
383
  for (const [key, value] of Object.entries(inputs)) {
384
  const fieldMatch = key.match(/^FIELD(\d+)$/);
385
  const keyMatch = key.match(/^KEY(\d+)$/);
386
+
387
  if (fieldMatch) {
388
  const index = parseInt(fieldMatch[1]);
389
  fieldCount = Math.max(fieldCount, index + 1);
 
393
  keyValues[index] = value;
394
  }
395
  }
396
+
397
  // Set up the mutator state
398
  if (fieldCount > 0) {
399
  newBlock.fieldCount_ = fieldCount;
400
  newBlock.fieldKeys_ = [];
401
+
402
  // Create the inputs through the mutator
403
  for (let i = 0; i < fieldCount; i++) {
404
  const keyValue = keyValues[i];
405
+ const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
406
+ ? keyValue.replace(/^["']|["']$/g, '')
407
  : `key${i}`;
408
+
409
  newBlock.fieldKeys_[i] = key;
410
+
411
  // Create the input
412
  const input = newBlock.appendValueInput('FIELD' + i);
413
  const field = new Blockly.FieldTextInput(key);
 
418
  input.appendField(field, 'KEY' + i);
419
  input.appendField(':');
420
  }
421
+
422
  // Now connect the field values
423
  for (let i = 0; i < fieldCount; i++) {
424
  const value = fieldValues[i];
425
  if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
426
  // This is a nested block, create it recursively
427
  const childBlock = parseAndCreateBlock(value);
428
+
429
  // Connect the child block to the FIELD input
430
  const input = newBlock.getInput('FIELD' + i);
431
  if (input && input.connection && childBlock.outputConnection) {
 
442
  if (value.match(/^\w+\s*\(inputs\(/)) {
443
  // This is a nested block, create it recursively
444
  const childBlock = parseAndCreateBlock(value);
445
+
446
  // Connect the child block to the appropriate input
447
  const input = newBlock.getInput(key);
448
  if (input && input.connection && childBlock.outputConnection) {
 
452
  // This is a simple value, set it as a field
453
  // Remove quotes if present
454
  const cleanValue = value.replace(/^["']|["']$/g, '');
455
+
456
  // Try to set as a field value
457
  try {
458
  newBlock.setFieldValue(cleanValue, key);
 
478
  }
479
  }
480
  }
481
+
482
  // Initialize the block (renders it)
483
  newBlock.initSvg();
484
+
485
  // Only position the top-level block
486
  if (shouldPosition) {
487
  // Find a good position that doesn't overlap existing blocks
488
  const existingBlocks = ws.getAllBlocks();
489
  let x = 50;
490
  let y = 50;
491
+
492
  // Simple positioning: stack new blocks vertically
493
  if (existingBlocks.length > 0) {
494
  const lastBlock = existingBlocks[existingBlocks.length - 1];
495
  const lastPos = lastBlock.getRelativeToSurfaceXY();
496
  y = lastPos.y + lastBlock.height + 20;
497
  }
498
+
499
  newBlock.moveBy(x, y);
500
  }
501
+
502
  // Render the block
503
  newBlock.render();
504
+
505
  return newBlock;
506
  }
507
+
508
  // Helper function to parse inputs(key: value, key2: value2, ...)
509
  function parseInputs(inputStr) {
510
  const result = {};
 
514
  let inQuotes = false;
515
  let quoteChar = '';
516
  let readingKey = true;
517
+
518
  for (let i = 0; i < inputStr.length; i++) {
519
  const char = inputStr[i];
520
+
521
  // Handle quotes
522
+ if ((char === '"' || char === "'") && (i === 0 || inputStr[i - 1] !== '\\')) {
523
  if (!inQuotes) {
524
  inQuotes = true;
525
  quoteChar = char;
 
528
  quoteChar = '';
529
  }
530
  }
531
+
532
  // Handle parentheses depth (for nested blocks)
533
  if (!inQuotes) {
534
  if (char === '(') depth++;
535
  else if (char === ')') depth--;
536
  }
537
+
538
  // Handle key-value separation
539
  if (char === ':' && depth === 0 && !inQuotes && readingKey) {
540
  readingKey = false;
541
  currentKey = currentKey.trim();
542
  continue;
543
  }
544
+
545
  // Handle comma separation
546
  if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
547
  // Store the key-value pair
548
  currentValue = currentValue.trim();
549
+
550
  // Parse the value
551
  if (currentValue.match(/^\w+\s*\(inputs\(/)) {
552
  // This is a nested block
 
561
  // This is a string (remove quotes if present)
562
  result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
563
  }
564
+
565
  // Reset for next key-value pair
566
  currentKey = '';
567
  currentValue = '';
568
  readingKey = true;
569
  continue;
570
  }
571
+
572
  // Accumulate characters
573
  if (readingKey) {
574
  currentKey += char;
 
576
  currentValue += char;
577
  }
578
  }
579
+
580
  // Handle the last key-value pair
581
  if (currentKey && currentValue) {
582
  currentKey = currentKey.trim();
583
  currentValue = currentValue.trim();
584
+
585
  // Parse the value
586
  if (currentValue.match(/^\w+\s*\(inputs\(/)) {
587
  // This is a nested block
 
597
  result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
598
  }
599
  }
600
+
601
  return result;
602
  }
603
+
604
  // Create the block and all its nested children
605
  const newBlock = parseAndCreateBlock(data.block_spec, true);
606
+
607
  if (newBlock) {
608
  blockId = newBlock.id;
609
+
610
  // If under_block_id is specified, attach the new block under the parent
611
  if (data.under_block_id) {
612
  const parentBlock = ws.getBlockById(data.under_block_id);
613
  if (parentBlock) {
614
  console.log('[SSE CREATE] Attaching to parent block:', data.under_block_id);
615
+
616
  // Find an appropriate input to connect to
617
  // Try common statement inputs first
618
  const statementInputs = ['BODY', 'DO', 'THEN', 'ELSE', 'STACK'];
619
  let connected = false;
620
+
621
  for (const inputName of statementInputs) {
622
  const input = parentBlock.getInput(inputName);
623
  if (input && input.type === Blockly.NEXT_STATEMENT) {
 
646
  }
647
  }
648
  }
649
+
650
  // If not connected to statement input, try value inputs
651
  if (!connected) {
652
  // Try all inputs
 
662
  }
663
  }
664
  }
665
+
666
  if (!connected) {
667
  console.warn('[SSE CREATE] Could not find suitable connection point on parent block');
668
  }
 
670
  console.warn('[SSE CREATE] Parent block not found:', data.under_block_id);
671
  }
672
  }
673
+
674
  success = true;
675
  console.log('[SSE CREATE] Successfully created block with children:', blockId, newBlock.type);
676
  } else {
677
  throw new Error(`Failed to create block from specification`);
678
  }
679
+
680
  } catch (e) {
681
  error = e.toString();
682
  console.error('[SSE CREATE] Error creating block:', e);
683
  }
684
+
685
  // Send result back to backend immediately
686
+ console.log('[SSE CREATE] Sending creation result:', {
687
+ request_id: data.request_id,
688
+ success,
689
  error,
690
+ block_id: blockId
691
  });
692
+
693
  fetch('/creation_result', {
694
  method: 'POST',
695
  headers: { 'Content-Type': 'application/json' },
 
709
  console.error('[SSE CREATE] Error processing message:', err);
710
  }
711
  };
712
+
713
  eventSource.onerror = (error) => {
714
  console.error('[SSE CREATE] Connection error:', error);
715
  // Reconnect after 5 seconds
 
718
  setupCreationStream();
719
  }, 5000);
720
  };
721
+
722
  eventSource.onopen = () => {
723
  console.log('[SSE CREATE] Connected to creation stream');
724
  };
 
730
  const setupVariableStream = () => {
731
  const eventSource = new EventSource('/variable_stream');
732
  const processedRequests = new Set(); // Track processed variable requests
733
+
734
  eventSource.onmessage = (event) => {
735
  try {
736
  const data = JSON.parse(event.data);
737
+
738
  // Skip heartbeat messages
739
  if (data.heartbeat) return;
740
+
741
  // Skip if we've already processed this request
742
  if (data.request_id && processedRequests.has(data.request_id)) {
743
  console.log('[SSE VARIABLE] Skipping duplicate variable request:', data.request_id);
 
748
  // Clear after 10 seconds to allow retries if needed
749
  setTimeout(() => processedRequests.delete(data.request_id), 10000);
750
  }
751
+
752
  if (data.variable_name && data.request_id) {
753
  console.log('[SSE VARIABLE] Received variable creation request:', data.request_id, data.variable_name);
754
+
755
  let success = false;
756
  let error = null;
757
  let variableId = null;
758
+
759
  try {
760
  // Create the variable using Blockly's variable map
761
  const variableName = data.variable_name;
762
+
763
  // Use the workspace's variable map to create a new variable
764
  const variableModel = ws.getVariableMap().createVariable(variableName);
765
+
766
  if (variableModel) {
767
  variableId = variableModel.getId();
768
  success = true;
 
770
  } else {
771
  throw new Error('Failed to create variable model');
772
  }
773
+
774
  } catch (e) {
775
  error = e.toString();
776
  console.error('[SSE VARIABLE] Error creating variable:', e);
777
  }
778
+
779
  // Send result back to backend immediately
780
+ console.log('[SSE VARIABLE] Sending variable creation result:', {
781
+ request_id: data.request_id,
782
+ success,
783
  error,
784
+ variable_id: variableId
785
  });
786
+
787
  fetch('/variable_result', {
788
  method: 'POST',
789
  headers: { 'Content-Type': 'application/json' },
 
803
  console.error('[SSE VARIABLE] Error processing message:', err);
804
  }
805
  };
806
+
807
  eventSource.onerror = (error) => {
808
  console.error('[SSE VARIABLE] Connection error:', error);
809
  // Reconnect after 5 seconds
 
812
  setupVariableStream();
813
  }, 5000);
814
  };
815
+
816
  eventSource.onopen = () => {
817
  console.log('[SSE VARIABLE] Connected to variable stream');
818
  };
 
831
  // Initialize the Python generator with the workspace before generating code
832
  // This ensures the Names database is properly set up for control flow blocks
833
  pythonGenerator.init(ws);
834
+
835
  // Instead of using workspaceToCode which processes ALL blocks,
836
  // manually process only blocks connected to create_mcp or func_def
837
  let code = '';
838
+
839
  // Get all top-level blocks (not connected to other blocks)
840
  const topBlocks = ws.getTopBlocks(false);
841
+
842
  // Process only create_mcp and func_def blocks
843
  for (const block of topBlocks) {
844
  if (block.type === 'create_mcp' || block.type === 'func_def') {
 
857
 
858
  const vars = ws.getVariableMap().getAllVariables();
859
  globalVarString = vars.map(v => `${v.id} | ${v.name}`).join("\n");
860
+
861
  const codeEl = document.querySelector('#generatedCode code');
862
 
863
  const call = `def llm_call(prompt, model):
 
967
  const response = await fetch("/update_chat", {
968
  method: "POST",
969
  headers: { "Content-Type": "application/json" },
970
+ body: JSON.stringify({
971
  code: chatCode,
972
  varString: globalVarString
973
  }),
974
  });
975
+
976
  if (response.ok) {
977
  chatBackendAvailable = true;
978
  console.log("[Blockly] Sent updated Chat code to backend");
 
982
  } catch (err) {
983
  console.warn(`[Blockly] Chat backend not ready (attempt ${retryCount + 1}):`, err.message);
984
  chatBackendAvailable = false;
985
+
986
  // Queue this update for retry
987
  if (retryCount < 5) {
988
  const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
 
1008
 
1009
  // You can add any chat-specific preprocessing here
1010
  // For example, adding headers or formatting
1011
+
1012
  if (codeEl) {
1013
  codeEl.textContent = globalChatCode;
1014
  }
 
1019
  } else {
1020
  // Queue the update and try to establish connection
1021
  chatUpdateQueue.push(globalChatCode);
1022
+
1023
  // Clear any existing retry timeout
1024
  if (chatRetryTimeout) {
1025
  clearTimeout(chatRetryTimeout);
1026
  }
1027
+
1028
  // Try to connect to backend
1029
  checkChatBackend();
1030
+
1031
  // Set up periodic retry
1032
  chatRetryTimeout = setTimeout(() => {
1033
  checkChatBackend();
 
1049
  if (!block.inputRefBlocks_) {
1050
  block.inputRefBlocks_ = new Map();
1051
  }
1052
+
1053
  // Create reference blocks for each input if they don't exist
1054
  if (block.inputNames_ && block.inputNames_.length > 0) {
1055
  for (let i = 0; i < block.inputNames_.length; i++) {
 
1059
  if (input && input.connection) {
1060
  const connectedBlock = input.connection.targetBlock();
1061
  const expectedType = `input_reference_${name}`;
1062
+
1063
  // If there's already the correct block connected, just track it
1064
  if (connectedBlock && connectedBlock.type === expectedType) {
1065
  connectedBlock._ownerBlockId = block.id;
1066
  connectedBlock.setDeletable(false);
1067
  block.inputRefBlocks_.set(name, connectedBlock);
1068
+ }
1069
  // Only create if input exists AND has no connected block yet
1070
  else if (!connectedBlock) {
1071
  // Create the reference block