owenkaplinsky
Squash commits
cc3b7b2
import * as Blockly from 'blockly';
import { blocks } from './blocks/text';
import { forBlock } from './generators/python';
import { pythonGenerator } from 'blockly/python';
import { chatGenerator, forBlock as chatForBlock } from './generators/chat';
import { save, load } from './serialization';
import { toolbox } from './toolbox';
import '@blockly/toolbox-search';
import DarkTheme from '@blockly/theme-dark';
import './index.css';
// Determine the correct base path when running behind HF proxy (/proxy/... or /spaces/...)
const getBasePath = () => {
const p = window.location.pathname || "";
if (p.startsWith("/proxy/")) {
const parts = p.split("/").filter(Boolean); // proxy/owner/space[/...]
return "/" + parts.slice(0, 3).join("/");
}
if (p.startsWith("/spaces/")) {
const parts = p.split("/").filter(Boolean); // spaces/owner/space[/...]
return "/" + parts.slice(0, 3).join("/");
}
return "";
};
const basePath = getBasePath();
// Session ID Handling
function getOrCreateSessionId() {
const STORAGE_KEY = "mcp_blockly_session_id";
let sessionId = window.localStorage.getItem(STORAGE_KEY);
if (!sessionId) {
if (window.crypto && window.crypto.randomUUID) {
sessionId = window.crypto.randomUUID();
} else {
sessionId = "sess_" + Math.random().toString(36).substring(2) + Date.now().toString(36);
}
window.localStorage.setItem(STORAGE_KEY, sessionId);
}
return sessionId;
}
const sessionId = getOrCreateSessionId();
window.sessionId = sessionId;
console.log("[SESSION] Using sessionId:", sessionId);
// Share session id with other mounted apps (e.g., Gradio tester) via cookie
const sameSite = window.location.protocol === "https:" ? "None" : "Lax";
const secure = window.location.protocol === "https:" ? "; Secure" : "";
const maxAge = 60 * 60 * 24 * 7; // one week
document.cookie = `mcp_blockly_session_id=${sessionId}; path=/; Max-Age=${maxAge}; SameSite=${sameSite}${secure}`;
// Ensure embedded Gradio iframes receive the session id even when third-party cookies are blocked
function attachSessionToIframes() {
const frames = [
document.getElementById("gradioTestFrame"),
document.getElementById("gradioChatFrame"),
];
frames.forEach((frame) => {
if (!frame) return;
const baseSrc = frame.dataset.baseSrc || frame.getAttribute("src") || "";
if (!baseSrc) return;
// Prefix proxy base when iframe src is absolute-from-root
const resolvedBaseSrc = baseSrc.startsWith("/") ? `${basePath}${baseSrc}` : baseSrc;
const joiner = baseSrc.includes("?") ? "&" : "?";
frame.src = `${resolvedBaseSrc}${joiner}session_id=${encodeURIComponent(sessionId)}&__theme=dark`;
});
}
attachSessionToIframes();
// Register the blocks and generator with Blockly
Blockly.common.defineBlocks(blocks);
Object.assign(pythonGenerator.forBlock, forBlock);
// Register chat generator blocks
Object.assign(chatGenerator.forBlock, chatForBlock);
// Set up UI elements and inject Blockly
const blocklyDiv = document.getElementById('blocklyDiv');
// Inject Blockly with theme + renderer
const ws = Blockly.inject(blocklyDiv, {
toolbox,
grid: {
spacing: 35,
length: 3,
colour: '#ccc',
snap: false
},
disable: false,
collapse: false,
zoom: {
controls: true,
wheel: true,
startScale: 1.0,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.2,
pinch: true
},
renderer: 'zelos',
theme: DarkTheme,
});
window.workspace = ws;
Blockly.ContextMenuItems.registerCommentOptions();
const newButton = document.querySelector('#newButton');
newButton.addEventListener("click", () => {
ws.clear()
// Create the MCP block
const mcpBlock = ws.newBlock('create_mcp');
mcpBlock.initSvg();
mcpBlock.setDeletable(false);
mcpBlock.setMovable(true); // Allow moving but not deleting
// Position it in a reasonable spot
mcpBlock.moveBy(50, 50);
mcpBlock.render();
});
loadButton.addEventListener("click", () => {
const input = document.createElement('input');
let fileContent;
input.type = 'file';
input.accept = '.txt'; // Specify the file types you want to accept
input.onchange = function (event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function (event) {
fileContent = JSON.parse(event.target.result); // Parse directly without decoding
Blockly.serialization.workspaces.load(fileContent, ws);
};
reader.readAsText(file);
};
input.click();
});
saveButton.addEventListener("click", () => {
const state = Blockly.serialization.workspaces.save(ws);
const stateString = JSON.stringify(state);
var filename = "mcpBlockly_project.txt";
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(stateString));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
});
// Download Code button
const downloadCodeButton = document.querySelector('#downloadCodeButton');
downloadCodeButton.addEventListener("click", () => {
// Get the current generated code
const codeEl = document.querySelector('#generatedCode code');
const code = codeEl ? codeEl.textContent : '';
if (!code) {
alert('No code to download');
return;
}
var filename = "app.py";
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(code));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
});
// Settings button and Keys Modal
const settingsButton = document.querySelector('#settingsButton');
const apiKeyModal = document.querySelector('#apiKeyModal');
// const apiKeyInput = document.querySelector('#apiKeyInput'); // TEMP: No OpenAI key input field
const hfKeyInput = document.querySelector('#hfKeyInput');
const saveApiKeyButton = document.querySelector('#saveApiKey');
const cancelApiKeyButton = document.querySelector('#cancelApiKey');
const OPENAI_KEY_STORAGE = "mcp_blockly_openai_key";
const HF_KEY_STORAGE = "mcp_blockly_hf_key";
const loadStoredKeys = () => {
const storedOpenAI = window.localStorage.getItem(OPENAI_KEY_STORAGE) || "";
const storedHF = window.localStorage.getItem(HF_KEY_STORAGE) || "";
// apiKeyInput.value = storedOpenAI; // TEMP: No OpenAI key input field
if (hfKeyInput) {
hfKeyInput.value = storedHF;
}
};
settingsButton.addEventListener("click", () => {
if (hfKeyInput) {
loadStoredKeys();
} else {
console.warn("[Settings] HF key input not found in DOM");
}
apiKeyModal.style.display = 'flex';
});
saveApiKeyButton.addEventListener("click", () => {
// const apiKey = apiKeyInput.value.trim(); // TEMP: No OpenAI key input field
const apiKey = ""; // TEMP: Using free API key, OpenAI field disabled
const hfKey = hfKeyInput?.value.trim() || "";
// Validate OpenAI key format if provided
if (apiKey && (!apiKey.startsWith("sk-") || apiKey.length < 40)) {
alert("Invalid OpenAI API key format. Please enter a valid OpenAI API key (starts with 'sk-').");
return;
}
// Validate Hugging Face key format if provided
if (hfKey && (!hfKey.startsWith("hf_") || hfKey.length < 20)) {
alert("Invalid Hugging Face API key format. Please enter a valid Hugging Face API key (starts with 'hf_').");
return;
}
// Save API keys locally
window.localStorage.setItem(OPENAI_KEY_STORAGE, apiKey);
window.localStorage.setItem(HF_KEY_STORAGE, hfKey);
// Share keys with backend via cookies (per-request, not stored server-side)
const cookieOpts = "path=/; SameSite=None; Secure";
if (apiKey) {
document.cookie = `mcp_openai_key=${encodeURIComponent(apiKey)}; ${cookieOpts}`;
} else {
document.cookie = `mcp_openai_key=; Max-Age=0; ${cookieOpts}`;
}
if (hfKey) {
document.cookie = `mcp_hf_key=${encodeURIComponent(hfKey)}; ${cookieOpts}`;
} else {
document.cookie = `mcp_hf_key=; Max-Age=0; ${cookieOpts}`;
}
alert('API keys saved successfully');
apiKeyModal.style.display = 'none';
});
cancelApiKeyButton.addEventListener("click", () => {
apiKeyModal.style.display = 'none';
});
// Welcome Modal Setup
const welcomeModal = document.querySelector('#welcomeModal');
// TEMPORARY FREE API KEY - welcomeApiKeyInput removed from DOM
// const welcomeApiKeyInput = document.querySelector('#welcomeApiKeyInput');
const welcomeHfKeyInput = document.querySelector('#welcomeHfKeyInput');
const saveWelcomeApiKeyButton = document.querySelector('#saveWelcomeApiKey');
const skipTutorialButton = document.querySelector('#skipTutorialButton');
const dontShowWelcomeAgainCheckbox = document.querySelector('#dontShowWelcomeAgain');
const WELCOME_DISMISSED_COOKIE = "mcp_blockly_welcome_dismissed";
const getCookieValue = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
};
const loadWelcomeStoredKeys = () => {
// TEMPORARY FREE API KEY
// const storedOpenAI = window.localStorage.getItem(OPENAI_KEY_STORAGE) || "";
const storedHF = window.localStorage.getItem(HF_KEY_STORAGE) || "";
// TEMPORARY FREE API KEY - welcomeApiKeyInput removed from DOM
// welcomeApiKeyInput.value = storedOpenAI;
welcomeHfKeyInput.value = storedHF;
};
const showWelcomeModal = () => {
loadWelcomeStoredKeys();
dontShowWelcomeAgainCheckbox.checked = false;
welcomeModal.style.display = 'flex';
};
const hideWelcomeModal = () => {
welcomeModal.style.display = 'none';
if (dontShowWelcomeAgainCheckbox.checked) {
const maxAge = 60 * 60 * 24 * 365; // one year
const sameSite = window.location.protocol === "https:" ? "None" : "Lax";
const secure = window.location.protocol === "https:" ? "; Secure" : "";
document.cookie = `${WELCOME_DISMISSED_COOKIE}=true; path=/; Max-Age=${maxAge}; SameSite=${sameSite}${secure}`;
}
};
saveWelcomeApiKeyButton.addEventListener("click", () => {
// TEMPORARY FREE API KEY
// const apiKey = welcomeApiKeyInput.value.trim();
const apiKey = ""; // TEMPORARY FREE API KEY - using free API
const hfKey = welcomeHfKeyInput.value.trim();
// TEMPORARY FREE API KEY
// // Validate OpenAI key format if provided
// if (apiKey && (!apiKey.startsWith("sk-") || apiKey.length < 40)) {
// alert("Invalid OpenAI API key format. Please enter a valid OpenAI API key (starts with 'sk-').");
// return;
// }
// Validate Hugging Face key format if provided
if (hfKey && (!hfKey.startsWith("hf_") || hfKey.length < 20)) {
alert("Invalid Hugging Face API key format. Please enter a valid Hugging Face API key (starts with 'hf_').");
return;
}
// Save API keys locally
window.localStorage.setItem(OPENAI_KEY_STORAGE, apiKey);
window.localStorage.setItem(HF_KEY_STORAGE, hfKey);
// Share keys with backend via cookies (per-request, not stored server-side)
const cookieOpts = "path=/; SameSite=None; Secure";
// TEMPORARY FREE API KEY
// if (apiKey) {
// document.cookie = `mcp_openai_key=${encodeURIComponent(apiKey)}; ${cookieOpts}`;
// } else {
// document.cookie = `mcp_openai_key=; Max-Age=0; ${cookieOpts}`;
// }
document.cookie = `mcp_openai_key=; Max-Age=0; ${cookieOpts}`; // TEMPORARY FREE API KEY - clear free API cookie
if (hfKey) {
document.cookie = `mcp_hf_key=${encodeURIComponent(hfKey)}; ${cookieOpts}`;
} else {
document.cookie = `mcp_hf_key=; Max-Age=0; ${cookieOpts}`;
}
hideWelcomeModal();
// Trigger the tutorial flow
tutorialEnabled = true;
completedTutorialStepIndex = -1;
examplesJustFlashed = false;
checkAndFlashExamplesButton();
});
skipTutorialButton.addEventListener("click", () => {
tutorialEnabled = false;
hideWelcomeModal();
});
// Show welcome modal on first visit
const welcomeDismissed = getCookieValue(WELCOME_DISMISSED_COOKIE);
console.log('[Welcome] Cookie value:', welcomeDismissed);
if (!welcomeDismissed) {
// Delay showing welcome to ensure page is fully loaded
setTimeout(() => {
console.log('[Welcome] Showing welcome modal');
showWelcomeModal();
}, 100);
} else {
console.log('[Welcome] Welcome modal dismissed, skipping');
}
const weatherText = '{"workspaceComments":[{"height":80,"width":477,"id":"XI5[EHp-Ow+kinXf6n5y","x":51.0743994140625,"y":-53.56000305175782,"text":"Gets temperature of location with a latitude and a longitude."}],"blocks":{"languageVersion":0,"blocks":[{"type":"create_mcp","id":")N.HEG1x]Z/,k#TeWr,S","x":50,"y":50,"deletable":false,"extraState":{"inputCount":2,"inputNames":["latitude","longitude"],"inputTypes":["float","float"],"outputCount":1,"outputNames":["output0"],"outputTypes":["float"],"toolCount":0},"inputs":{"X0":{"block":{"type":"input_reference_latitude","id":"]3mj!y}qfRt+!okheU7L","deletable":false,"extraState":{"ownerBlockId":")N.HEG1x]Z/,k#TeWr,S"},"fields":{"VARNAME":"latitude"}}},"X1":{"block":{"type":"input_reference_longitude","id":"Do/{HFNGSd.!;POiKS?D","deletable":false,"extraState":{"ownerBlockId":")N.HEG1x]Z/,k#TeWr,S"},"fields":{"VARNAME":"longitude"}}},"R0":{"block":{"type":"cast_as","id":"vKz#fsrWMW(M9*:3Pv;2","fields":{"TYPE":"float"},"inputs":{"VALUE":{"block":{"type":"in_json","id":"R|j?_8s^H{l0;UZ-oQt3","inputs":{"NAME":{"block":{"type":"text","id":"@Z+@U^@8c0gQYj}La`PY","fields":{"TEXT":"temperature_2m"}}},"JSON":{"block":{"type":"in_json","id":"X=M,R1@7bRjJVZIPi[qD","inputs":{"NAME":{"block":{"type":"text","id":"OMr~`#kG$3@k`YPDHbzH","fields":{"TEXT":"current"}}},"JSON":{"block":{"type":"call_api","id":"^(.vyM.yni08S~c1EBm=","fields":{"METHOD":"GET"},"inputs":{"URL":{"shadow":{"type":"text","id":"}.T;_U_OsRS)B_y09p % { ","fields":{"TEXT":""}},"block":{"type":"text_replace","id":"OwH9uERJPTGQG!UER#ch","inputs":{"FROM":{"shadow":{"type":"text","id":"ya05#^ 7 % UbUeXX#eDSmH","fields":{"TEXT":"{latitude}"}},"block":{"type":"text","id":"6CX#+wo9^x+vZ`LRt5ms","fields":{"TEXT":"{latitude}"}}},"TO":{"shadow":{"type":"text","id":": _ZloQuh9c-MNf-U]!k5","fields":{"TEXT":""}},"block":{"type":"cast_as","id":"qXXp2GSF;@+ssDvHN={+","fields":{"TYPE":"str"},"inputs":{"VALUE":{"block":{"type":"input_reference_latitude","id":"?%@)3sErZ)}=#4ags#gu","extraState":{"ownerBlockId":")N.HEG1x]Z/,k#TeWr,S"},"fields":{"VARNAME":"latitude"}}}}}},"TEXT":{"shadow":{"type":"text","id":"w@zsP)m6:WjkUp,ln3$x","fields":{"TEXT":""}},"block":{"type":"text_replace","id":"ImNPsvzD7r^+1MJ%IirV","inputs":{"FROM":{"shadow":{"type":"text","id":"%o(3rro?WLIFpmE0#MMM","fields":{"TEXT":"{longitude}"}},"block":{"type":"text","id":"`p!s8dQ7e~?0JvofyB-{","fields":{"TEXT":"{longitude}"}}},"TO":{"shadow":{"type":"text","id":"Zpql-%oJ_sdSi | r |* er | ","fields":{"TEXT":""}},"block":{"type":"cast_as","id":"T5r7Y,;]kq2wClH)JUf8","fields":{"TYPE":"str"},"inputs":{"VALUE":{"block":{"type":"input_reference_longitude","id":"WUgiJP$X + zY#f$5nhnTX","extraState":{"ownerBlockId":") N.HEG1x]Z /, k#TeWr, S"},"fields":{"VARNAME":"longitude"}}}}}},"TEXT":{"shadow":{"type":"text","id":", (vw$o_s7P = b4P; 8]}yj","fields":{"TEXT":"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m,wind_speed_10m"}}}}}}}}}}}}}}}}}}}}}}}]}}';
let examplesJustFlashed = false;
let selectedExample = null; // Track which example was selected
let tutorialEnabled = false; // Track if tutorial is enabled
// Tutorial popup management
const tutorialStepOrder = ['examples', 'refresh', 'test', 'aiAssistant', 'send'];
let completedTutorialStepIndex = -1;
const getTutorialStepIndex = (step) => tutorialStepOrder.indexOf(step);
const hasCompletedStepOrBeyond = (step) => {
const idx = getTutorialStepIndex(step);
return idx !== -1 && completedTutorialStepIndex >= idx;
};
const markTutorialStepComplete = (step) => {
const idx = getTutorialStepIndex(step);
if (idx > completedTutorialStepIndex) {
completedTutorialStepIndex = idx;
}
};
let currentTutorialStep = null;
const tutorialPopup = document.getElementById('tutorialPopup');
const tutorialTitle = document.getElementById('tutorialTitle');
const tutorialBody = document.getElementById('tutorialBody');
const tutorialSkipButton = document.getElementById('tutorialSkipButton');
console.log('[TUTORIAL] Popup elements:', { tutorialPopup, tutorialTitle, tutorialBody, tutorialSkipButton });
const tutorialContent = {
examples: {
title: '1/5 Try an Example',
body: 'Click on one of the example workflows to get started quickly.'
},
refresh: {
title: '2/5 Refresh Info',
body: 'Click the Refresh button to update the inputs and outputs of your MCP server.'
},
test: {
title: '3/5 Run Your Test',
get body() {
if (selectedExample === 'weather') {
return 'Enter a latitude (-90 to 90) and longitude (-180 to 80), then press Test to get the current temperature at that location!';
} else if (selectedExample === 'fact') {
return 'Enter a claim you want to verify, then press Test to check if it\'s true!';
}
return 'Enter some values in your MCP inputs, and press the Test button to see its output.';
}
},
aiAssistant: {
title: '4/5 Get AI Guidance',
body: 'After your output is generated, switch to the AI Assistant tab to chat with the integrated agent.'
},
send: {
title: '5/5 Send a Message',
body: 'Try using one of the three suggested prompts (click on one of the buttons), or write one of your own!'
}
};
const showTutorialPopup = (step, targetElement) => {
console.log('[TUTORIAL] showTutorialPopup called for step:', step, 'element:', targetElement);
if (!tutorialContent[step]) {
console.log('[TUTORIAL] Content not found for step:', step);
return;
}
currentTutorialStep = step;
tutorialTitle.textContent = tutorialContent[step].title;
// Handle both regular string body and getter function
const bodyContent = typeof tutorialContent[step].body === 'function'
? tutorialContent[step].body()
: tutorialContent[step].body;
tutorialBody.textContent = bodyContent;
tutorialPopup.style.display = 'block';
console.log('[TUTORIAL] Popup display set to block');
// Position the popup next to the target element
if (targetElement) {
setTimeout(() => {
const rect = targetElement.getBoundingClientRect();
const popupWidth = 280;
const popupHeight = 150; // Approximate height
const spacing = 25;
console.log('[TUTORIAL] Target element rect:', rect, 'window:', window.innerWidth, 'x', window.innerHeight);
let top = rect.top;
let left = rect.right + spacing;
// Priority 1: Try to place to the right
if (left + popupWidth > window.innerWidth) {
// Priority 2: Try to place below
left = rect.left;
top = rect.bottom + spacing;
if (top + popupHeight > window.innerHeight) {
// Priority 3: Place to the left
left = rect.left - popupWidth - spacing;
top = rect.top;
}
}
console.log('[TUTORIAL] Setting popup position to top:', top, 'left:', left);
tutorialPopup.style.top = `${top}px`;
tutorialPopup.style.left = `${left}px`;
}, 50);
}
};
const hideTutorialPopup = () => {
tutorialPopup.style.display = 'none';
currentTutorialStep = null;
};
tutorialSkipButton.addEventListener('click', () => {
// Stop the tutorial and remove all flashing indicators (including inside iframes)
tutorialEnabled = false;
examplesJustFlashed = false;
document.querySelectorAll('.flash-button').forEach(el => el.classList.remove('flash-button'));
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc?.querySelectorAll('.flash-button').forEach(el => el.classList.remove('flash-button'));
} catch (e) {
// Ignore cross-origin frames
}
}
hideTutorialPopup();
});
const loadSendButtonFlash = () => {
if (!tutorialEnabled) return;
// Find send button in the chat iframe (it's the button with just an SVG icon)
let sendBtn = null;
let iframeDoc = null;
console.log('[TUTORIAL] Looking for send button in iframes');
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
// Look for send button - it's the one with SVG viewBox="0 0 22 24"
const buttons = iframeDoc.querySelectorAll('button');
console.log('[TUTORIAL] Found', buttons.length, 'buttons in iframe');
for (let btn of buttons) {
// Look for the button with the send SVG (has viewBox="0 0 22 24")
if (btn.innerHTML.includes('viewBox="0 0 22 24"')) {
console.log('[TUTORIAL] Found send button with SVG!');
sendBtn = btn;
break;
}
}
if (sendBtn) break;
}
} catch (e) {
// Cannot access iframe, continue
console.log('[TUTORIAL] Cannot access iframe:', e);
}
}
console.log('[TUTORIAL] Send button found?', !!sendBtn);
if (sendBtn && iframeDoc) {
// Inject CSS if needed
const styleId = 'send-flash-style';
if (!iframeDoc.getElementById(styleId)) {
const style = iframeDoc.createElement('style');
style.id = styleId;
style.textContent = `
@keyframes pulseOutline {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 1);
}
75% {
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
.flash-button {
animation: pulseOutline 1s ease-out infinite !important;
}
`;
iframeDoc.head.appendChild(style);
}
sendBtn.classList.add('flash-button');
showTutorialPopup('send', sendBtn);
// Watch for when the send button gets removed/replaced (which happens when message is sent)
const observer = new MutationObserver((mutations) => {
console.log('[TUTORIAL] Send button DOM changed - message was sent');
hideTutorialPopup();
observer.disconnect();
});
// Observe the button's parent for changes (including removal/replacement)
if (sendBtn.parentElement) {
observer.observe(sendBtn.parentElement, {
childList: true,
subtree: true
});
}
}
};
const loadAiAssistantTabFlash = () => {
if (!tutorialEnabled || hasCompletedStepOrBeyond('aiAssistant')) return;
const aiTab = document.querySelector('[data-tab="aichat"]');
if (aiTab) {
aiTab.classList.add('flash-button');
showTutorialPopup('aiAssistant', aiTab);
// Remove flash on click
const removeFlash = () => {
aiTab.classList.remove('flash-button');
aiTab.removeEventListener('click', removeFlash);
markTutorialStepComplete('aiAssistant');
hideTutorialPopup();
// After AI Assistant tab is clicked, flash the send button
setTimeout(() => {
loadSendButtonFlash();
}, 100);
};
aiTab.addEventListener('click', removeFlash);
}
};
const loadExamplesButtonFlash = () => {
if (!tutorialEnabled || hasCompletedStepOrBeyond('examples')) return;
const examplesButton = Array.from(document.querySelectorAll('.menuButton')).find(btn => btn.textContent === 'Examples');
console.log('[TUTORIAL] Examples button found:', examplesButton);
if (examplesButton) {
examplesButton.classList.add('flash-button');
examplesJustFlashed = true;
console.log('[TUTORIAL] Showing examples popup');
showTutorialPopup('examples', examplesButton);
} else {
console.log('[TUTORIAL] Examples button NOT found');
}
};
const loadTestButtonFlash = (testBtn, iframeDoc) => {
if (!tutorialEnabled || hasCompletedStepOrBeyond('test') || !testBtn || !iframeDoc) return;
if (testBtn && iframeDoc) {
testBtn.classList.add('flash-button');
showTutorialPopup('test', testBtn);
// Remove flash on click
const removeFlash = () => {
testBtn.classList.remove('flash-button');
testBtn.removeEventListener('click', removeFlash);
markTutorialStepComplete('test');
hideTutorialPopup();
// Proceed to the next step
loadAiAssistantTabFlash();
};
testBtn.addEventListener('click', removeFlash);
}
};
const loadRefreshButtonFlash = () => {
if (!tutorialEnabled || hasCompletedStepOrBeyond('refresh')) return;
let refreshBtn = null;
let testBtn = null;
let iframeDoc = null;
// Check iframes for the Refresh and Test buttons
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
const iframeButtons = iframeDoc.querySelectorAll('button');
// Try to find Refresh and Test buttons in this iframe
for (let btn of iframeButtons) {
if (btn.textContent.includes('Refresh') || btn.innerHTML.includes('Refresh')) {
refreshBtn = btn;
}
if (btn.textContent.includes('Test') && btn.classList.contains('secondary')) {
testBtn = btn;
}
if (refreshBtn && testBtn) break;
}
if (refreshBtn && testBtn) break;
}
} catch (e) {
// Cannot access iframe, continue
}
}
if (refreshBtn && iframeDoc) {
// Inject CSS into the iframe if needed
const styleId = 'refresh-flash-style';
if (!iframeDoc.getElementById(styleId)) {
const style = iframeDoc.createElement('style');
style.id = styleId;
style.textContent = `
@keyframes pulseOutline {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 1);
}
75% {
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
.flash-button {
animation: pulseOutline 1s ease-out infinite !important;
}
`;
iframeDoc.head.appendChild(style);
}
refreshBtn.classList.add('flash-button');
showTutorialPopup('refresh', refreshBtn);
// Remove flash on click
const removeFlash = () => {
refreshBtn.classList.remove('flash-button');
refreshBtn.removeEventListener('click', removeFlash);
markTutorialStepComplete('refresh');
hideTutorialPopup();
// After Refresh is clicked, flash the Test button
if (testBtn) {
loadTestButtonFlash(testBtn, iframeDoc);
}
};
refreshBtn.addEventListener('click', removeFlash);
} else {
// Retry after a delay if button not found yet
setTimeout(loadRefreshButtonFlash, 1000);
}
};
const checkAndFlashExamplesButton = () => {
console.log('[TUTORIAL] checkAndFlashExamplesButton called, tutorialEnabled:', tutorialEnabled, 'examplesJustFlashed:', examplesJustFlashed);
if (!tutorialEnabled) {
console.log('[TUTORIAL] Tutorial disabled, skipping');
return;
}
if (hasCompletedStepOrBeyond('examples')) {
console.log('[TUTORIAL] Examples step already completed, skipping');
return;
}
if (!examplesJustFlashed) {
// Flash the examples button immediately
console.log('[TUTORIAL] About to call loadExamplesButtonFlash');
loadExamplesButtonFlash();
}
};
weatherButton.addEventListener("click", () => {
try {
selectedExample = 'weather';
const fileContent = JSON.parse(weatherText);
Blockly.serialization.workspaces.load(fileContent, ws);
const shouldHandleTutorial = tutorialEnabled && !hasCompletedStepOrBeyond('examples');
if (shouldHandleTutorial) {
// Example loaded - stop flashing Examples button and move to Refresh
const examplesButton = Array.from(document.querySelectorAll('.menuButton')).find(btn => btn.textContent === 'Examples');
if (examplesButton) {
examplesButton.classList.remove('flash-button');
}
hideTutorialPopup();
examplesJustFlashed = false;
markTutorialStepComplete('examples');
// Flash refresh button when example is selected
loadRefreshButtonFlash();
}
} catch (error) {
console.error("Error loading weather.txt contents:", error);
}
});
const factText = "{\"workspaceComments\":[{\"height\":66,\"width\":575,\"id\":\"x/Z2E2Oid(4||-pQ)h*;\",\"x\":51.00000000000023,\"y\":-35.76388082917071,\"text\":\"A fact checker that uses a searching LLM to verify the validity of a claim.\"}],\"blocks\":{\"languageVersion\":0,\"blocks\":[{\"type\":\"create_mcp\",\"id\":\"yScKJD/XLhk)D}qn2TW:\",\"x\":50,\"y\":50,\"deletable\":false,\"extraState\":{\"inputCount\":1,\"inputNames\":[\"prompt\"],\"inputTypes\":[\"string\"],\"outputCount\":1,\"outputNames\":[\"result\"],\"outputTypes\":[\"string\"],\"toolCount\":0},\"inputs\":{\"X0\":{\"block\":{\"type\":\"input_reference_prompt\",\"id\":\"-r%M-[oX1]?RxxF_V(V@\",\"deletable\":false,\"extraState\":{\"ownerBlockId\":\"yScKJD/XLhk)D}qn2TW:\"},\"fields\":{\"VARNAME\":\"prompt\"}}},\"R0\":{\"block\":{\"type\":\"llm_call\",\"id\":\"m/*D8ZBx;QZlUN*aw15U\",\"fields\":{\"MODEL\":\"gpt-4o-search-preview-2025-03-11\"},\"inputs\":{\"PROMPT\":{\"block\":{\"type\":\"text_join\",\"id\":\"e@#`RVKXpIZ9__%zUK]`\",\"extraState\":{\"itemCount\":3},\"inputs\":{\"ADD0\":{\"block\":{\"type\":\"text\",\"id\":\"M3QD})k`FXiizaF,gA{9\",\"fields\":{\"TEXT\":\"Verify whether the following claim: \\\"\"}}},\"ADD1\":{\"block\":{\"type\":\"input_reference_prompt\",\"id\":\"B4.LNZ0es`RFM0Xi@SL:\",\"extraState\":{\"ownerBlockId\":\"yScKJD/XLhk)D}qn2TW:\"},\"fields\":{\"VARNAME\":\"prompt\"}}},\"ADD2\":{\"block\":{\"type\":\"text\",\"id\":\"Ng!fFR+xTMdmgWZv6Oh{\",\"fields\":{\"TEXT\":\"\\\" is true or not. Return one of the following values: \\\"True\\\", \\\"Unsure\\\", \\\"False\\\", and nothing else. You may not say anything but one of these answers no matter what.\"}}}}}}}}}}}]}}"
factButton.addEventListener("click", () => {
try {
selectedExample = 'fact';
const fileContent = JSON.parse(factText);
Blockly.serialization.workspaces.load(fileContent, ws);
const shouldHandleTutorial = tutorialEnabled && !hasCompletedStepOrBeyond('examples');
if (shouldHandleTutorial) {
// Example loaded - stop flashing Examples button and move to Refresh
const examplesButton = Array.from(document.querySelectorAll('.menuButton')).find(btn => btn.textContent === 'Examples');
if (examplesButton) {
examplesButton.classList.remove('flash-button');
}
hideTutorialPopup();
examplesJustFlashed = false;
markTutorialStepComplete('examples');
// Flash refresh button when example is selected
loadRefreshButtonFlash();
}
} catch (error) {
console.error("Error loading weather.txt contents:", error);
}
});
undoButton.addEventListener("click", () => {
ws.undo(false);
});
redoButton.addEventListener("click", () => {
ws.undo(true);
});
cleanWorkspace.addEventListener("click", () => {
ws.cleanUp();
});
function parseAndCreateBlock(spec, shouldPosition = false, placementType = null, placementBlockID = null) {
// Match block_name(inputs(...)) with proper parenthesis matching
const blockMatch = spec.match(/^(\w+)\s*\((.*)$/s);
if (!blockMatch) {
throw new Error(`Invalid block specification format: ${spec}`);
}
const blockType = blockMatch[1];
let content = blockMatch[2].trim();
// We need to find the matching closing parenthesis for blockType(
// Count from the beginning and find where the matching ) is
let parenCount = 1; // We already have the opening (
let matchIndex = -1;
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
// Handle quotes
if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
if (!inQuotes) {
inQuotes = true;
quoteChar = char;
} else if (char === quoteChar) {
inQuotes = false;
}
}
// Only count parens outside quotes
if (!inQuotes) {
if (char === '(') parenCount++;
else if (char === ')') {
parenCount--;
if (parenCount === 0) {
matchIndex = i;
break;
}
}
}
}
// Extract content up to the matching closing paren
if (matchIndex >= 0) {
content = content.slice(0, matchIndex).trim();
} else {
// Fallback: remove last paren if present
if (content.endsWith(')')) {
content = content.slice(0, -1).trim();
}
}
console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
// Check if this has inputs() wrapper
let inputsContent = content;
if (content.startsWith('inputs(')) {
// Find the matching closing parenthesis for inputs(
parenCount = 1;
matchIndex = -1;
inQuotes = false;
quoteChar = '';
for (let i = 7; i < content.length; i++) { // Start after 'inputs('
const char = content[i];
// Handle quotes
if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
if (!inQuotes) {
inQuotes = true;
quoteChar = char;
} else if (char === quoteChar) {
inQuotes = false;
}
}
// Only count parens outside quotes
if (!inQuotes) {
if (char === '(') parenCount++;
else if (char === ')') {
parenCount--;
if (parenCount === 0) {
matchIndex = i;
break;
}
}
}
}
// Extract content between inputs( and its matching )
if (matchIndex >= 0) {
inputsContent = content.slice(7, matchIndex);
} else {
// Fallback: remove inputs( and last )
if (content.endsWith(')')) {
inputsContent = content.slice(7, -1);
} else {
inputsContent = content.slice(7);
}
}
}
console.log('[SSE CREATE] inputsContent to parse:', inputsContent);
// VALIDATION: Check if trying to place a value block under a statement block
// Value blocks have an output connection but no previous connection
if (placementType === 'under') {
// Check if this block type is a value block by temporarily creating it
const testBlock = ws.newBlock(blockType);
const isValueBlock = testBlock.outputConnection && !testBlock.previousConnection;
testBlock.dispose(true); // Remove the test block
if (isValueBlock) {
throw new Error(`Cannot place value block '${blockType}' under a statement block. Value blocks must be nested inside inputs of other blocks or placed in MCP outputs using type: "input". Try creating a variable and assigning the value to that.`);
}
}
// Create the block
const newBlock = ws.newBlock(blockType);
if (inputsContent) {
// Parse the inputs content
console.log('[SSE CREATE] About to call parseInputs with:', inputsContent);
const inputs = parseInputs(inputsContent);
console.log('[SSE CREATE] Parsed inputs:', inputs);
// Special handling for make_json block
if (blockType === 'make_json') {
// Count FIELD entries to determine how many fields we need
let fieldCount = 0;
const fieldValues = {};
const keyValues = {};
for (const [key, value] of Object.entries(inputs)) {
const fieldMatch = key.match(/^FIELD(\d+)$/);
const keyMatch = key.match(/^KEY(\d+)$/);
if (fieldMatch) {
const index = parseInt(fieldMatch[1]);
fieldCount = Math.max(fieldCount, index + 1);
fieldValues[index] = value;
} else if (keyMatch) {
const index = parseInt(keyMatch[1]);
keyValues[index] = value;
}
}
// Set up the mutator state
if (fieldCount > 0) {
newBlock.fieldCount_ = fieldCount;
newBlock.fieldKeys_ = [];
// Create the inputs through the mutator
for (let i = 0; i < fieldCount; i++) {
const keyValue = keyValues[i];
const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
? keyValue.replace(/^["']|["']$/g, '')
: `key${i}`;
newBlock.fieldKeys_[i] = key;
// Create the input
const input = newBlock.appendValueInput('FIELD' + i);
const field = new Blockly.FieldTextInput(key);
field.setValidator((newValue) => {
newBlock.fieldKeys_[i] = newValue || `key${i}`;
return newValue;
});
input.appendField(field, 'KEY' + i);
input.appendField(':');
}
// Now connect the field values
for (let i = 0; i < fieldCount; i++) {
const value = fieldValues[i];
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
// This is a nested block, create it recursively
const childBlock = parseAndCreateBlock(value);
// Connect the child block to the FIELD input
const input = newBlock.getInput('FIELD' + i);
if (input && input.connection && childBlock.outputConnection) {
childBlock.outputConnection.connect(input.connection);
}
}
}
}
} else if (blockType === 'text_join') {
// Special handling for text_join block (and similar blocks with ADD0, ADD1, ADD2...)
// Count ADD entries to determine how many items we need
let addCount = 0;
const addValues = {};
for (const [key, value] of Object.entries(inputs)) {
const addMatch = key.match(/^ADD(\d+)$/);
if (addMatch) {
const index = parseInt(addMatch[1]);
addCount = Math.max(addCount, index + 1);
addValues[index] = value;
}
}
console.log('[SSE CREATE] text_join detected with', addCount, 'items');
// Store pending text_join state to apply after initSvg()
if (addCount > 0) {
newBlock.pendingAddCount_ = addCount;
newBlock.pendingAddValues_ = addValues;
}
} else if (blockType === 'controls_if') {
// Special handling for if/else blocks - create condition blocks now and store references
const conditionBlocks = {};
const conditionBlockObjects = {};
let hasElse = false;
console.log('[SSE CREATE] controls_if inputs:', inputs);
// Process condition inputs and store block objects
// Blockly uses IF0, IF1, IF2... not IF, IFELSEN0, IFELSEN1
for (const [key, value] of Object.entries(inputs)) {
if (key.match(/^IF\d+$/)) {
// This is a condition block specification (IF0, IF1, IF2, ...)
conditionBlocks[key] = value;
if (typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
// Create the condition block now
const conditionBlock = parseAndCreateBlock(value);
conditionBlockObjects[key] = conditionBlock;
console.log('[SSE CREATE] Created condition block for', key);
}
} else if (key === 'ELSE' && value === true) {
// ELSE is a marker with no value (set to true by parseInputs)
console.log('[SSE CREATE] Detected ELSE marker');
hasElse = true;
}
}
// Count IFELSE (else-if) blocks: IF1, IF2, IF3... (IF0 is the main if, not an else-if)
let elseIfCount = 0;
for (const key of Object.keys(conditionBlocks)) {
if (key.match(/^IF\d+$/) && key !== 'IF0') {
elseIfCount++;
}
}
console.log('[SSE CREATE] controls_if parsed: elseIfCount =', elseIfCount, 'hasElse =', hasElse);
// Store condition block OBJECTS for later - we'll connect them after mutator creates inputs
newBlock.pendingConditionBlockObjects_ = conditionBlockObjects;
newBlock.pendingElseifCount_ = elseIfCount;
newBlock.pendingElseCount_ = hasElse ? 1 : 0;
console.log('[SSE CREATE] Stored pending condition block objects:', Object.keys(conditionBlockObjects));
// Skip normal input processing for controls_if - we handle conditions after mutator
} else if (blockType !== 'controls_if') {
// Normal block handling (skip for controls_if which is handled specially)
for (const [key, value] of Object.entries(inputs)) {
if (typeof value === 'string') {
// Check if this is a nested block specification
if (value.match(/^\w+\s*\(inputs\(/)) {
// This is a nested block, create it recursively
const childBlock = parseAndCreateBlock(value);
// Connect the child block to the appropriate input
const input = newBlock.getInput(key);
if (input && input.connection && childBlock.outputConnection) {
childBlock.outputConnection.connect(input.connection);
}
} else {
// This is a simple value, set it as a field
// Remove quotes if present
const cleanValue = value.replace(/^["']|["']$/g, '');
// Try to set as a field value
try {
newBlock.setFieldValue(cleanValue, key);
} catch (e) {
console.log(`[SSE CREATE] Could not set field ${key} to ${cleanValue}:`, e);
}
}
} else if (typeof value === 'number') {
// Set numeric field value
try {
newBlock.setFieldValue(value, key);
} catch (e) {
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
}
} else if (typeof value === 'boolean') {
// Set boolean field value
try {
newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
} catch (e) {
console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
}
}
}
}
}
// Initialize the block (renders it)
newBlock.initSvg();
// Apply pending controls_if mutations (must be after initSvg)
try {
console.log('[SSE CREATE] Checking for controls_if mutations: type =', newBlock.type, 'pendingElseifCount_ =', newBlock.pendingElseifCount_, 'pendingConditionBlockObjects_ =', !!newBlock.pendingConditionBlockObjects_);
if (newBlock.type === 'controls_if' && (newBlock.pendingElseifCount_ > 0 || newBlock.pendingElseCount_ > 0 || newBlock.pendingConditionBlockObjects_)) {
console.log('[SSE CREATE] ENTERING controls_if mutation block');
console.log('[SSE CREATE] Applying controls_if mutation:', {
elseifCount: newBlock.pendingElseifCount_,
elseCount: newBlock.pendingElseCount_
});
// Use the loadExtraState method if available (Blockly's preferred way)
if (typeof newBlock.loadExtraState === 'function') {
const state = {};
if (newBlock.pendingElseifCount_ > 0) {
state.elseIfCount = newBlock.pendingElseifCount_;
}
if (newBlock.pendingElseCount_ > 0) {
state.hasElse = true;
}
console.log('[SSE CREATE] Using loadExtraState with:', state);
newBlock.loadExtraState(state);
console.log('[SSE CREATE] After loadExtraState');
} else {
// Fallback: Set the internal state variables and call updateShape_
newBlock.elseifCount_ = newBlock.pendingElseifCount_;
newBlock.elseCount_ = newBlock.pendingElseCount_;
if (typeof newBlock.updateShape_ === 'function') {
console.log('[SSE CREATE] Calling updateShape_ on controls_if');
newBlock.updateShape_();
}
}
// Now that the mutator has created all the inputs, connect the stored condition block objects
console.log('[SSE CREATE] pendingConditionBlockObjects_ exists?', !!newBlock.pendingConditionBlockObjects_);
if (newBlock.pendingConditionBlockObjects_) {
const conditionBlockObjects = newBlock.pendingConditionBlockObjects_;
console.log('[SSE CREATE] Connecting condition blocks:', Object.keys(conditionBlockObjects));
// Connect the IF0 condition
if (conditionBlockObjects['IF0']) {
const ifBlock = conditionBlockObjects['IF0'];
const input = newBlock.getInput('IF0');
console.log('[SSE CREATE] IF0 input exists?', !!input);
if (input && input.connection && ifBlock.outputConnection) {
ifBlock.outputConnection.connect(input.connection);
console.log('[SSE CREATE] Connected IF0 condition');
} else {
console.warn('[SSE CREATE] Could not connect IF0 - input:', !!input, 'childConnection:', !!ifBlock.outputConnection);
}
}
// Connect IF1, IF2, IF3... (else-if conditions)
console.log('[SSE CREATE] Processing', newBlock.pendingElseifCount_, 'else-if conditions');
for (let i = 1; i <= newBlock.pendingElseifCount_; i++) {
const key = 'IF' + i;
console.log('[SSE CREATE] Looking for key:', key, 'exists?', !!conditionBlockObjects[key]);
if (conditionBlockObjects[key]) {
const ifElseBlock = conditionBlockObjects[key];
const input = newBlock.getInput(key);
console.log('[SSE CREATE] Input', key, 'exists?', !!input);
if (input && input.connection && ifElseBlock.outputConnection) {
ifElseBlock.outputConnection.connect(input.connection);
console.log('[SSE CREATE] Connected', key, 'condition');
} else {
console.warn('[SSE CREATE] Could not connect', key, '- input exists:', !!input, 'has connection:', input ? !!input.connection : false, 'childHasOutput:', !!ifElseBlock.outputConnection);
}
}
}
} else {
console.warn('[SSE CREATE] No pendingConditionBlockObjects_ found');
}
// Verify the ELSE input was created
if (newBlock.pendingElseCount_ > 0) {
const elseInput = newBlock.getInput('ELSE');
console.log('[SSE CREATE] ELSE input after mutation:', elseInput);
if (!elseInput) {
console.error('[SSE CREATE] ELSE input was NOT created!');
}
}
// Re-render after connecting condition blocks
newBlock.render();
}
} catch (err) {
console.error('[SSE CREATE] Error in controls_if mutations:', err);
}
// Apply pending text_join mutations (must be after initSvg)
if (newBlock.type === 'text_join' && newBlock.pendingAddCount_ && newBlock.pendingAddCount_ > 0) {
console.log('[SSE CREATE] Applying text_join mutation with', newBlock.pendingAddCount_, 'items');
const addCount = newBlock.pendingAddCount_;
const addValues = newBlock.pendingAddValues_;
// Use loadExtraState if available to set the item count
if (typeof newBlock.loadExtraState === 'function') {
newBlock.loadExtraState({ itemCount: addCount });
} else {
// Fallback: set internal state
newBlock.itemCount_ = addCount;
}
// Now connect the ADD values
for (let i = 0; i < addCount; i++) {
const value = addValues[i];
if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
// This is a nested block, create it recursively
const childBlock = parseAndCreateBlock(value);
// Connect the child block to the ADD input
const input = newBlock.getInput('ADD' + i);
if (input && input.connection && childBlock.outputConnection) {
childBlock.outputConnection.connect(input.connection);
console.log('[SSE CREATE] Connected ADD' + i + ' input');
} else {
console.warn('[SSE CREATE] Could not connect ADD' + i + ' input');
}
}
}
}
// Only position the top-level block
if (shouldPosition) {
// Find a good position that doesn't overlap existing blocks
const existingBlocks = ws.getAllBlocks();
let x = 50;
let y = 50;
// Simple positioning: stack new blocks vertically
if (existingBlocks.length > 0) {
const lastBlock = existingBlocks[existingBlocks.length - 1];
const lastPos = lastBlock.getRelativeToSurfaceXY();
y = lastPos.y + lastBlock.height + 20;
}
newBlock.moveBy(x, y);
}
// Render the block
newBlock.render();
return newBlock;
}
// Helper function to parse inputs(key: value, key2: value2, ...)
function parseInputs(inputStr) {
const result = {};
let currentKey = '';
let currentValue = '';
let depth = 0;
let inQuotes = false;
let quoteChar = '';
let readingKey = true;
for (let i = 0; i < inputStr.length; i++) {
const char = inputStr[i];
// Handle quotes
if ((char === '"' || char === "'") && (i === 0 || inputStr[i - 1] !== '\\')) {
if (!inQuotes) {
inQuotes = true;
quoteChar = char;
} else if (char === quoteChar) {
inQuotes = false;
quoteChar = '';
}
}
// Handle parentheses depth (for nested blocks)
if (!inQuotes) {
if (char === '(') depth++;
else if (char === ')') depth--;
}
// Handle key-value separation
if (char === ':' && depth === 0 && !inQuotes && readingKey) {
readingKey = false;
currentKey = currentKey.trim();
continue;
}
// Handle comma separation
if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
// Store the key-value pair
currentValue = currentValue.trim();
// Parse the value
if (currentValue.match(/^\w+\s*\(inputs\(/)) {
// This is a nested block
result[currentKey] = currentValue;
} else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
// This is a number
result[currentKey] = parseFloat(currentValue);
} else if (currentValue === 'true' || currentValue === 'false') {
// This is a boolean
result[currentKey] = currentValue === 'true';
} else {
// This is a string (remove quotes if present)
result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
}
// Reset for next key-value pair
currentKey = '';
currentValue = '';
readingKey = true;
continue;
}
// Accumulate characters
if (readingKey) {
currentKey += char;
} else {
currentValue += char;
}
}
// Handle the last key-value pair
if (currentKey) {
currentKey = currentKey.trim();
// If there's no value, this is a flag/marker (like ELSE)
if (!currentValue) {
result[currentKey] = true; // Mark it as present
} else {
currentValue = currentValue.trim();
// Parse the value
if (currentValue.match(/^\w+\s*\(inputs\(/)) {
// This is a nested block
result[currentKey] = currentValue;
} else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
// This is a number
result[currentKey] = parseFloat(currentValue);
} else if (currentValue === 'true' || currentValue === 'false') {
// This is a boolean
result[currentKey] = currentValue === 'true';
} else {
// This is a string (remove quotes if present)
result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
}
}
}
return result;
}
// Set up unified SSE connection for all workspace operations
const setupUnifiedStream = () => {
const esUrl = `${basePath}/unified_stream?session_id=${sessionId}`;
console.log("[SSE] Opening unified_stream", esUrl);
const eventSource = new EventSource(esUrl);
const processedRequests = new Set(); // Track processed requests
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Skip heartbeat messages
if (data.heartbeat) return;
// Determine request key based on type
let requestKey;
if (data.type === 'delete') {
requestKey = `delete_${data.block_id}`;
} else if (data.type === 'create') {
requestKey = `create_${data.request_id}`;
} else if (data.type === 'variable') {
requestKey = `variable_${data.request_id}`;
} else if (data.type === 'edit_mcp') {
requestKey = `edit_mcp_${data.request_id}`;
} else if (data.type === 'replace') {
requestKey = `replace_${data.request_id}`;
}
// Skip if we've already processed this request
if (requestKey && processedRequests.has(requestKey)) {
console.log('[SSE] Skipping duplicate request:', requestKey);
return;
}
if (requestKey) {
processedRequests.add(requestKey);
// Clear after 10 seconds to allow retries if needed
setTimeout(() => processedRequests.delete(requestKey), 10000);
}
// Handle edit MCP requests
if (data.type === 'edit_mcp' && data.request_id) {
console.log('[SSE] Received edit MCP request:', data);
let success = false;
let error = null;
try {
// Find the create_mcp block
const mcpBlocks = ws.getBlocksByType('create_mcp');
const mcpBlock = mcpBlocks[0];
if (!mcpBlock) {
throw new Error('No create_mcp block found in workspace');
}
// Disable events to prevent infinite loops
Blockly.Events.disable();
try {
// Create a container block for the mutator
const containerBlock = ws.newBlock('container');
containerBlock.initSvg();
// Build inputs if provided
if (data.inputs && Array.isArray(data.inputs)) {
let connection = containerBlock.getInput('STACK').connection;
for (let idx = 0; idx < data.inputs.length; idx++) {
const input = data.inputs[idx];
const itemBlock = ws.newBlock('container_input');
itemBlock.initSvg();
itemBlock.setFieldValue(input.type || 'string', 'TYPE');
itemBlock.setFieldValue(input.name || '', 'NAME');
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
}
// Build outputs if provided
if (data.outputs && Array.isArray(data.outputs)) {
let connection2 = containerBlock.getInput('STACK2').connection;
for (let idx = 0; idx < data.outputs.length; idx++) {
const output = data.outputs[idx];
const itemBlock = ws.newBlock('container_output');
itemBlock.initSvg();
itemBlock.setFieldValue(output.type || 'string', 'TYPE');
itemBlock.setFieldValue(output.name || 'output', 'NAME');
connection2.connect(itemBlock.previousConnection);
connection2 = itemBlock.nextConnection;
}
}
// Apply changes using the compose method
mcpBlock.compose(containerBlock);
// Clean up
containerBlock.dispose();
success = true;
console.log('[SSE] Successfully edited MCP block');
} finally {
Blockly.Events.enable();
}
} catch (e) {
error = e.toString();
console.error('[SSE] Error editing MCP block:', e);
}
// Send result back to backend immediately
console.log('[SSE] Sending edit MCP result:', { request_id: data.request_id, success, error });
fetch(`${basePath}/request_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
request_type: 'edit_mcp',
request_id: data.request_id,
success: success,
error: error
})
}).then(response => {
console.log('[SSE] Edit MCP result sent successfully');
}).catch(err => {
console.error('[SSE] Error sending edit MCP result:', err);
});
}
// Handle replace block requests
else if (data.type === 'replace' && data.block_id && data.block_spec && data.request_id) {
console.log('[SSE] Received replace request for block:', data.block_id, data.block_spec);
let success = false;
let error = null;
let blockId = null;
try {
// Get the block to be replaced
const blockToReplace = ws.getBlockById(data.block_id);
if (!blockToReplace) {
throw new Error(`Block ${data.block_id} not found`);
}
// Store connection info before creating new block
const parentBlock = blockToReplace.getParent();
const previousBlock = blockToReplace.getPreviousBlock();
const nextBlock = blockToReplace.getNextBlock();
let parentConnection = null;
let inputName = null;
// Check if this block is connected to a parent's input
if (blockToReplace.outputConnection && blockToReplace.outputConnection.targetConnection) {
parentConnection = blockToReplace.outputConnection.targetConnection;
} else if (blockToReplace.previousConnection && blockToReplace.previousConnection.targetConnection) {
parentConnection = blockToReplace.previousConnection.targetConnection;
}
// If the block is in an input socket, get that info
if (parentBlock) {
const inputs = parentBlock.inputList;
for (const input of inputs) {
if (input.connection && input.connection.targetBlock() === blockToReplace) {
inputName = input.name;
break;
}
}
}
// Preserve only statement/next blocks (the body), not input field values
const statementBlocks = {};
const oldInputList = blockToReplace.inputList;
for (const input of oldInputList) {
// Only preserve STATEMENT type inputs (like BODY, SUBSTACK, etc) - these are the internal structure
// Skip VALUE inputs and other types that might contain data values
if (input.type === Blockly.NEXT_STATEMENT && input.connection && input.connection.targetBlock()) {
const childBlock = input.connection.targetBlock();
statementBlocks[input.name] = {
block: childBlock,
connection: input.connection
};
console.log('[SSE] Found statement input to preserve:', input.name);
}
}
// Create the new block using the shared function (no positioning, no placement type for replace)
const newBlock = parseAndCreateBlock(data.block_spec, false, null, null);
if (!newBlock) {
throw new Error('Failed to create replacement block');
}
// Reattach the new block to the parent connection
if (parentConnection) {
if (newBlock.outputConnection) {
parentConnection.connect(newBlock.outputConnection);
} else if (newBlock.previousConnection) {
parentConnection.connect(newBlock.previousConnection);
}
}
// Reattach next block if it was connected
if (nextBlock && newBlock.nextConnection) {
newBlock.nextConnection.connect(nextBlock.previousConnection);
}
// Transfer only statement blocks (body/internal structure) to matching inputs on new block
console.log('[SSE] Transferring', Object.keys(statementBlocks).length, 'statement blocks to new block');
for (const [oldInputName, blockInfo] of Object.entries(statementBlocks)) {
const newInput = newBlock.getInput(oldInputName);
if (newInput && newInput.connection && newInput.type === Blockly.NEXT_STATEMENT) {
console.log('[SSE] Transferring statement input:', oldInputName);
// Disconnect from old parent
blockInfo.block.unplug(false); // false = don't dispose children
// Connect to new parent
if (newInput.connection.targetBlock()) {
// Input already has something connected, skip
console.log('[SSE] Input', oldInputName, 'already has children, skipping');
} else {
newInput.connection.connect(blockInfo.block.previousConnection);
}
} else {
console.log('[SSE] New block does not have matching STATEMENT input:', oldInputName);
// Disconnect the child block from the old parent but leave it orphaned
// (it will appear as a separate block in the workspace)
blockInfo.block.unplug(false);
}
}
// Dispose only the old block itself, not its children (dispose(false))
blockToReplace.dispose(false);
// Render the workspace
ws.render();
success = true;
blockId = newBlock.id;
console.log('[SSE] Successfully replaced block:', data.block_id, 'with:', newBlock.id);
} catch (e) {
error = e.toString();
console.error('[SSE] Error replacing block:', e);
}
// Send result back to backend
console.log('[SSE] Sending replace block result:', { request_id: data.request_id, success, error, block_id: blockId });
fetch(`${basePath}/request_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
request_type: 'replace',
request_id: data.request_id,
success: success,
error: error,
block_id: blockId
})
}).then(response => {
console.log('[SSE] Replace block result sent successfully');
}).catch(err => {
console.error('[SSE] Error sending replace block result:', err);
});
}
// Handle deletion requests
else if (data.type === 'delete' && data.block_id) {
console.log('[SSE] Received deletion request for block:', data.block_id);
// Try to delete the block
const block = ws.getBlockById(data.block_id);
let success = false;
let error = null;
if (block) {
console.log('[SSE] Found block to delete:', block.type, block.id);
// Check if it's the main create_mcp block (which shouldn't be deleted)
if (block.type === 'create_mcp' && !block.isDeletable()) {
error = 'Cannot delete the main create_mcp block';
console.log('[SSE] Block is protected create_mcp');
} else {
try {
block.dispose(true);
success = true;
console.log('[SSE] Successfully deleted block:', data.block_id);
} catch (e) {
error = e.toString();
console.error('[SSE] Error deleting block:', e);
}
}
} else {
error = 'Block not found';
console.log('[SSE] Block not found:', data.block_id);
}
// Send result back to backend immediately
console.log('[SSE] Sending deletion result:', { block_id: data.block_id, success, error });
fetch(`${basePath}/request_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
request_type: 'delete',
block_id: data.block_id,
success: success,
error: error
})
}).then(response => {
console.log('[SSE] Deletion result sent successfully');
}).catch(err => {
console.error('[SSE] Error sending deletion result:', err);
});
}
// Handle creation requests
else if (data.type === 'create' && data.block_spec && data.request_id) {
console.log('[SSE] Received creation request:', data.request_id, data.block_spec);
let success = false;
let error = null;
let blockId = null;
try {
// Create the block and all its nested children
const newBlock = parseAndCreateBlock(data.block_spec, true, data.placement_type, data.blockID);
if (newBlock) {
blockId = newBlock.id;
success = true; // Block was created successfully
// Handle placement based on placement_type
if (data.placement_type === 'input') {
// Place into MCP block's output slot
// For type: 'input', find the first MCP block and use input_name for the slot
const mcpBlock = ws.getBlocksByType('create_mcp')[0];
if (mcpBlock) {
let inputSlot = data.input_name;
// If slot name is not in R format, look it up by output name
if (inputSlot && !inputSlot.match(/^R\d+$/)) {
const outputNames = mcpBlock.outputNames_ || [];
const outputIndex = outputNames.indexOf(inputSlot);
if (outputIndex >= 0) {
inputSlot = 'R' + outputIndex;
}
}
const input = mcpBlock.getInput(inputSlot);
if (input && input.connection) {
console.log('[SSE CREATE] Placing block into MCP output slot:', inputSlot);
// Disconnect any existing block
const existingBlock = input.connection.targetBlock();
if (existingBlock) {
existingBlock.unplug();
}
// Connect the new block
if (newBlock.outputConnection) {
input.connection.connect(newBlock.outputConnection);
console.log('[SSE CREATE] Successfully placed block into slot:', inputSlot);
} else {
error = `Block has no output connection to connect to MCP slot ${inputSlot}`;
console.error('[SSE CREATE]', error);
}
} else {
// Try to get all available inputs on the MCP block for debugging
const availableInputs = mcpBlock.inputList.map(inp => inp.name).join(', ');
error = `Output slot '${inputSlot}' not found. Available inputs: ${availableInputs}`;
console.error('[SSE CREATE]', error);
}
} else {
error = `No MCP block found in workspace`;
console.error('[SSE CREATE]', error);
}
}
// If placement_type is 'under', attach the new block under the parent
else if (data.placement_type === 'under') {
const parentBlock = ws.getBlockById(data.blockID);
if (parentBlock) {
console.log('[SSE CREATE] Attaching to parent block:', data.blockID);
// If input_name is specified, try to connect to that specific input first
let connected = false;
if (data.input_name) {
const input = parentBlock.getInput(data.input_name);
if (input && input.type === Blockly.NEXT_STATEMENT) {
// Check if something is already connected
if (input.connection && !input.connection.targetBlock()) {
// Connect directly
if (newBlock.previousConnection) {
input.connection.connect(newBlock.previousConnection);
connected = true;
console.log('[SSE CREATE] Connected to specified input:', data.input_name);
}
} else if (input.connection && input.connection.targetBlock()) {
// Find the last block in the stack
let lastBlock = input.connection.targetBlock();
while (lastBlock.nextConnection && lastBlock.nextConnection.targetBlock()) {
lastBlock = lastBlock.nextConnection.targetBlock();
}
// Connect to the end of the stack
if (lastBlock.nextConnection && newBlock.previousConnection) {
lastBlock.nextConnection.connect(newBlock.previousConnection);
connected = true;
console.log('[SSE CREATE] Connected to end of stack in specified input:', data.input_name);
}
}
} else {
error = `Specified input '${data.input_name}' not found or is not a statement input`;
console.warn('[SSE CREATE]', error);
}
}
// If not connected via specified input_name, try common statement inputs
if (!connected) {
const statementInputs = ['BODY', 'DO', 'THEN', 'ELSE', 'STACK'];
for (const inputName of statementInputs) {
const input = parentBlock.getInput(inputName);
if (input && input.type === Blockly.NEXT_STATEMENT) {
// Check if something is already connected
if (input.connection && !input.connection.targetBlock()) {
// Connect directly
if (newBlock.previousConnection) {
input.connection.connect(newBlock.previousConnection);
connected = true;
console.log('[SSE CREATE] Connected to input:', inputName);
break;
}
} else if (input.connection && input.connection.targetBlock()) {
// Find the last block in the stack
let lastBlock = input.connection.targetBlock();
while (lastBlock.nextConnection && lastBlock.nextConnection.targetBlock()) {
lastBlock = lastBlock.nextConnection.targetBlock();
}
// Connect to the end of the stack
if (lastBlock.nextConnection && newBlock.previousConnection) {
lastBlock.nextConnection.connect(newBlock.previousConnection);
connected = true;
console.log('[SSE CREATE] Connected to end of stack in input:', inputName);
break;
}
}
}
}
}
// If not connected to statement input, try value inputs
if (!connected) {
// Try all inputs
const inputs = parentBlock.inputList;
for (const input of inputs) {
if (input.type === Blockly.INPUT_VALUE && input.connection && !input.connection.targetBlock()) {
if (newBlock.outputConnection) {
input.connection.connect(newBlock.outputConnection);
connected = true;
console.log('[SSE CREATE] Connected to value input:', input.name);
break;
}
}
}
}
if (!connected) {
error = `Could not find suitable connection point on parent block`;
console.warn('[SSE CREATE]', error);
}
} else {
error = `Parent block not found: ${data.blockID}`;
console.warn('[SSE CREATE]', error);
}
}
if (success) {
console.log('[SSE CREATE] Successfully created block with children:', blockId, newBlock.type);
}
} else {
throw new Error(`Failed to create block from specification`);
}
} catch (e) {
error = e.toString();
console.error('[SSE CREATE] Error creating block:', e);
}
// Send result back to backend immediately
console.log('[SSE CREATE] Sending creation result:', {
request_id: data.request_id,
success,
error,
block_id: blockId
});
fetch(`${basePath}/request_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
request_type: 'create',
request_id: data.request_id,
success: success,
error: error,
block_id: blockId
})
}).then(response => {
console.log('[SSE CREATE] Creation result sent successfully');
}).catch(err => {
console.error('[SSE CREATE] Error sending creation result:', err);
});
}
// Handle variable creation requests
else if (data.type === 'variable' && data.variable_name && data.request_id) {
console.log('[SSE] Received variable creation request:', data.request_id, data.variable_name);
let success = false;
let error = null;
let variableId = null;
try {
// Create the variable using Blockly's variable map
const variableName = data.variable_name;
// Use the workspace's variable map to create a new variable
const variableModel = ws.getVariableMap().createVariable(variableName);
if (variableModel) {
variableId = variableModel.getId();
success = true;
console.log('[SSE] Successfully created variable:', variableName, 'with ID:', variableId);
} else {
throw new Error('Failed to create variable model');
}
} catch (e) {
error = e.toString();
console.error('[SSE] Error creating variable:', e);
}
// Send result back to backend immediately
console.log('[SSE] Sending variable creation result:', {
request_id: data.request_id,
success,
error,
variable_id: variableId
});
fetch(`${basePath}/request_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
request_type: 'variable',
request_id: data.request_id,
success: success,
error: error,
variable_id: variableId
})
}).then(response => {
console.log('[SSE] Variable creation result sent successfully');
}).catch(err => {
console.error('[SSE] Error sending variable creation result:', err);
});
}
} catch (err) {
console.error('[SSE] Error processing message:', err);
}
};
eventSource.onerror = (error) => {
console.error('[SSE] Connection error:', error);
// Reconnect after 5 seconds
setTimeout(() => {
console.log('[SSE] Attempting to reconnect...');
setupUnifiedStream();
}, 5000);
};
eventSource.onopen = () => {
console.log('[SSE] Connected to unified stream');
};
};
// Start the unified SSE connection
setupUnifiedStream();
// Observe any size change to the blockly container
const observer = new ResizeObserver(() => {
Blockly.svgResize(ws);
});
observer.observe(blocklyDiv);
const updateCode = () => {
// 1) Create isolated temporary workspace
const tempWs = new Blockly.Workspace();
// 2) Copy only allowed root blocks into the temp workspace
const topBlocks = ws.getTopBlocks(false);
for (const block of topBlocks) {
if (block.type === 'create_mcp' || block.type === 'func_def') {
Blockly.serialization.blocks.append(
Blockly.serialization.blocks.save(block),
tempWs
);
}
}
// 3) Generate code from the clean workspace
let code = pythonGenerator.workspaceToCode(tempWs);
// 4) Prepend helper functions collected during generation
const defs = pythonGenerator.definitions_
? Object.values(pythonGenerator.definitions_)
: [];
if (defs.length) {
code = defs.join('\n') + '\n\n' + code;
}
// Variable map (unchanged)
const vars = ws.getVariableMap().getAllVariables();
globalVarString = vars.map(v => `↿ ${v.id}${v.name}`).join("\n");
const codeEl = document.querySelector('#generatedCode code');
// Your custom helpers
const call = `def llm_call(prompt, model):
from openai import OpenAI
import os
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
return "Error: OpenAI API key not configured. Please set it in your HuggingFace Space env in its settings."
client = OpenAI(api_key=api_key)
messages = [{"role": "user", "content": prompt}]
completion = client.chat.completions.create(model=model, messages=messages)
return completion.choices[0].message.content.strip()
`;
const API = `def call_api(url, method="GET", headers={}):
import requests
response = requests.request(method, url, headers=headers)
data = response.json()
return data
`;
const blocks = ws.getAllBlocks(false);
const hasCall = blocks.some(block => block.type === 'llm_call');
const hasAPI = blocks.some(block => block.type === 'call_api');
const hasPrime = code.includes('math_isPrime(');
if (hasCall) code = call + code;
if (hasAPI) code = API + code;
if (hasPrime) {
code = code.replace(/math_isPrime\(([^)]*)\)/g, 'isprime($1)');
code = "from sympy import isprime\n\n" + code;
}
code = "import gradio as gr\nimport ast\n" + code;
// Extract input and output counts from the create_mcp block to build Gradio interface
const mcpBlocks = ws.getBlocksByType('create_mcp');
if (mcpBlocks.length > 0) {
const mcpBlock = mcpBlocks[0];
// Build list of Gradio input components based on input types
const gradioInputs = [];
if (mcpBlock.inputCount_ && mcpBlock.inputCount_ > 0 && mcpBlock.getInput('X0')) {
for (let k = 0; k < mcpBlock.inputCount_; k++) {
const type = mcpBlock.inputTypes_[k];
switch (type) {
case 'integer':
gradioInputs.push('gr.Number()');
break;
case 'float':
gradioInputs.push('gr.Number()');
break;
case 'string':
gradioInputs.push('gr.Textbox()');
break;
case 'list':
gradioInputs.push('gr.Textbox()');
break;
case 'boolean':
gradioInputs.push('gr.Checkbox()');
break;
case 'any':
gradioInputs.push('gr.JSON()');
break;
default:
gradioInputs.push('gr.Textbox()');
}
}
}
// Build list of Gradio output components based on output types
const gradioOutputs = [];
if (mcpBlock.outputCount_ && mcpBlock.outputCount_ > 0 && mcpBlock.getInput('R0')) {
for (let k = 0; k < mcpBlock.outputCount_; k++) {
const type = mcpBlock.outputTypes_[k];
switch (type) {
case 'integer':
gradioOutputs.push('gr.Number()');
break;
case 'float':
gradioOutputs.push('gr.Number()');
break;
case 'string':
gradioOutputs.push('gr.Textbox()');
break;
case 'list':
gradioOutputs.push('gr.Dataframe()');
break;
case 'boolean':
gradioOutputs.push('gr.Checkbox()');
break;
case 'any':
gradioOutputs.push('gr.JSON()');
break;
default:
gradioOutputs.push('gr.Textbox()');
}
}
}
// Append Gradio interface code at the very end
code += `\ndemo = gr.Interface(
fn=create_mcp,
inputs=[${gradioInputs.join(', ')}],
outputs=[${gradioOutputs.join(', ')}],
)
demo.launch(mcp_server=True)
`;
}
if (codeEl) {
codeEl.textContent = code;
}
fetch(`${basePath}/update_code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sessionId, code }),
}).catch((err) => {
console.error("[Blockly] Error sending Python code:", err);
});
};
// Track if chat backend is available
let chatBackendAvailable = false;
let chatUpdateQueue = [];
let chatRetryTimeout = null;
// Global variables for chat code and variables
let globalChatCode = '';
let globalVarString = '';
// Function to check if chat backend is available
const checkChatBackend = async () => {
try {
const response = await fetch(`${basePath}/update_chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session_id: sessionId,
code: globalChatCode,
varString: globalVarString
}),
});
if (response.ok) {
chatBackendAvailable = true;
console.log("[Blockly] Chat backend is available");
// Process any queued updates
processChatUpdateQueue();
return true;
}
} catch (err) {
chatBackendAvailable = false;
}
return false;
};
// Process queued chat updates
const processChatUpdateQueue = () => {
if (chatBackendAvailable && chatUpdateQueue.length > 0) {
const code = chatUpdateQueue.pop(); // Get the latest update
chatUpdateQueue = []; // Clear the queue
sendChatUpdate(code);
}
};
// Send chat update with retry logic
const sendChatUpdate = async (chatCode, retryCount = 0) => {
try {
const response = await fetch(`${basePath}/update_chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session_id: sessionId,
code: chatCode,
varString: globalVarString
}),
});
if (response.ok) {
chatBackendAvailable = true;
console.log("[Blockly] Sent updated Chat code to backend");
} else {
throw new Error(`Server responded with status ${response.status}`);
}
} catch (err) {
console.warn(`[Blockly] Chat backend not ready (attempt ${retryCount + 1}):`, err.message);
chatBackendAvailable = false;
// Queue this update for retry
if (retryCount < 5) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
setTimeout(() => {
if (!chatBackendAvailable) {
checkChatBackend().then(available => {
if (available) {
sendChatUpdate(chatCode, retryCount + 1);
} else if (retryCount < 4) {
sendChatUpdate(chatCode, retryCount + 1);
}
});
}
}, delay);
}
}
};
// Update function for the Chat generator (AI Assistant tab)
const updateChatCode = () => {
globalChatCode = chatGenerator.workspaceToCode(ws);
const codeEl = document.querySelector('#aichatCode code');
// You can add any chat-specific preprocessing here
// For example, adding headers or formatting
if (codeEl) {
codeEl.textContent = globalChatCode;
}
// If backend is available, send immediately
if (chatBackendAvailable) {
sendChatUpdate(globalChatCode);
} else {
// Queue the update and try to establish connection
chatUpdateQueue.push(globalChatCode);
// Clear any existing retry timeout
if (chatRetryTimeout) {
clearTimeout(chatRetryTimeout);
}
// Try to connect to backend
checkChatBackend();
// Set up periodic retry
chatRetryTimeout = setTimeout(() => {
checkChatBackend();
}, 2000);
}
};
try {
load(ws);
// After loading, ensure reference blocks are properly connected and tracked
setTimeout(() => {
const mutatorBlocks = ws.getAllBlocks(false).filter(b =>
(b.type === 'create_mcp' || b.type === 'func_def')
);
for (const block of mutatorBlocks) {
// Initialize the reference block map if needed
if (!block.inputRefBlocks_) {
block.inputRefBlocks_ = new Map();
}
// Create reference blocks for each input if they don't exist
if (block.inputNames_ && block.inputNames_.length > 0) {
for (let i = 0; i < block.inputNames_.length; i++) {
const name = block.inputNames_[i];
const input = block.getInput('X' + i);
if (input && input.connection) {
const connectedBlock = input.connection.targetBlock();
const expectedType = `input_reference_${name}`;
// If there's already the correct block connected, just track it
if (connectedBlock && connectedBlock.type === expectedType) {
connectedBlock._ownerBlockId = block.id;
connectedBlock.setDeletable(false);
block.inputRefBlocks_.set(name, connectedBlock);
}
// Only create if input exists AND has no connected block yet
else if (!connectedBlock) {
// Create the reference block
const refBlock = ws.newBlock(expectedType);
refBlock.initSvg();
refBlock.setDeletable(false);
refBlock._ownerBlockId = block.id;
refBlock.render();
// Connect it
if (refBlock.outputConnection) {
input.connection.connect(refBlock.outputConnection);
}
// Track it
block.inputRefBlocks_.set(name, refBlock);
}
}
}
}
}
ws.render();
}, 100);
} catch (e) {
console.warn('Workspace load failed, clearing storage:', e);
localStorage.clear();
}
// Ensure there's always one MCP block in the workspace
const existingMcpBlocks = ws.getBlocksByType('create_mcp');
if (existingMcpBlocks.length === 0) {
// Create the MCP block
const mcpBlock = ws.newBlock('create_mcp');
mcpBlock.initSvg();
mcpBlock.setDeletable(false);
mcpBlock.setMovable(true); // Allow moving but not deleting
// Position it in a reasonable spot
mcpBlock.moveBy(50, 50);
mcpBlock.render();
}
updateCode();
// Check if chat backend is available before first update
checkChatBackend().then(() => {
updateChatCode();
});
// Also set up periodic health checks for the chat backend
setInterval(() => {
if (!chatBackendAvailable) {
checkChatBackend();
}
}, 5000); // Check every 5 seconds if not connected
ws.addChangeListener((e) => {
if (e.isUiEvent) return;
save(ws);
});
ws.addChangeListener((event) => {
if (event.isUiEvent) return;
if (event.type === Blockly.Events.BLOCK_MOVE) {
if (event.oldParentId && !event.newParentId) {
const removedBlock = ws.getBlockById(event.blockId);
const oldParent = ws.getBlockById(event.oldParentId);
if (
removedBlock &&
oldParent &&
(removedBlock.type.startsWith('input_reference_') && (oldParent.type === 'create_mcp' || oldParent.type === 'func_def'))
) {
// Only duplicate if removed from a mutator input (X0, X1, X2, etc.)
// NOT from other inputs like RETURN, BODY, or title input
const inputName = event.oldInputName;
const isMutatorInput = inputName && /^X\d+$/.test(inputName);
if (isMutatorInput) {
Blockly.Events.disable();
try {
// Create a new block of the same reference type
const newRefBlock = ws.newBlock(removedBlock.type);
newRefBlock.initSvg();
newRefBlock.setDeletable(false); // This one stays in the MCP block
// Mark the new reference block with its owner (same as the parent)
newRefBlock._ownerBlockId = oldParent.id;
const input = oldParent.getInput(inputName);
if (input) {
input.connection.connect(newRefBlock.outputConnection);
}
// Update the parent block's reference tracking
if (removedBlock.type.startsWith('input_reference_')) {
const varName = removedBlock.type.replace('input_reference_', '');
if (oldParent.inputRefBlocks_) {
oldParent.inputRefBlocks_.set(varName, newRefBlock);
}
}
// Make the dragged-out block deletable
removedBlock.setDeletable(true);
ws.render(); // ensure workspace updates immediately
} finally {
Blockly.Events.enable();
}
}
}
}
}
save(ws);
});
ws.addChangeListener((e) => {
if (e.isUiEvent || e.type == Blockly.Events.FINISHED_LOADING || ws.isDragging()) {
return;
}
updateCode();
updateChatCode();
});