function showModal(id) { const modal = document.getElementById(id); if (modal) { modal.classList.add('visible'); if (id === 'open-modal') { htmx.trigger('#file-list-content', 'load'); } } } function hideModal(id) { const modal = document.getElementById(id); if (modal) { modal.classList.remove('visible'); } } function saveDesign() { const nodesData = Array.from(state.nodes.values()); document.getElementById('nodes-data').value = JSON.stringify(nodesData); document.getElementById('connections-data').value = JSON.stringify(state.connections); if (state.driveSource) { saveToDrive(); } else { htmx.ajax('POST', '/api/designer/save', { source: document.getElementById('designer-data'), target: '#status-message' }); } } async function saveToDrive() { const basCode = generateBasCode(); const { bucket, path } = state.driveSource; try { const response = await fetch('/api/files/write', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bucket, path, content: basCode }) }); if (response.ok) { const statusEl = document.querySelector('.status-item span'); if (statusEl) { statusEl.textContent = `Saved: ${path.split('/').pop()}`; } } else { const err = await response.json(); alert(`Save failed: ${err.error || 'Unknown error'}`); } } catch (e) { alert(`Save failed: ${e.message}`); } } function generateBasCode() { let basCode = "' Generated by General Bots Designer\n"; basCode += "' " + new Date().toISOString() + "\n\n"; const sortedNodes = Array.from(state.nodes.values()).sort((a, b) => { if (Math.abs(a.y - b.y) < 30) return a.x - b.x; return a.y - b.y; }); sortedNodes.forEach(node => { switch (node.type) { case 'TALK': basCode += `TALK "${node.fields.message || ''}"\n`; break; case 'HEAR': basCode += `HEAR ${node.fields.variable || 'input'} AS ${node.fields.type || 'string'}\n`; break; case 'SET': basCode += `SET ${node.fields.variable || 'x'} = ${node.fields.expression || '0'}\n`; break; case 'IF': basCode += `IF ${node.fields.condition || 'true'} THEN\n`; break; case 'FOR': basCode += `FOR EACH ${node.fields.variable || 'item'} IN ${node.fields.collection || 'items'}\n`; break; case 'CALL': basCode += `CALL ${node.fields.procedure || 'sub'}(${node.fields.arguments || ''})\n`; break; case 'SEND MAIL': basCode += `SEND MAIL TO "${node.fields.to || ''}" SUBJECT "${node.fields.subject || ''}" BODY "${node.fields.body || ''}"\n`; break; case 'GET': basCode += `GET ${node.fields.url || 'url'} TO ${node.fields.variable || 'result'}\n`; break; case 'POST': basCode += `POST ${node.fields.url || 'url'} WITH ${node.fields.body || '{}'} TO ${node.fields.variable || 'result'}\n`; break; case 'SAVE': basCode += `SAVE ${node.fields.data || 'data'} TO "${node.fields.filename || 'file.txt'}"\n`; break; case 'WAIT': basCode += `WAIT ${node.fields.duration || '1000'}\n`; break; case 'SET BOT MEMORY': basCode += `SET BOT MEMORY "${node.fields.key || 'key'}", ${node.fields.value || '""'}\n`; break; case 'GET BOT MEMORY': basCode += `GET BOT MEMORY "${node.fields.key || 'key'}" TO ${node.fields.variable || 'value'}\n`; break; case 'SET USER MEMORY': basCode += `SET USER MEMORY "${node.fields.key || 'key'}", ${node.fields.value || '""'}\n`; break; case 'GET USER MEMORY': basCode += `GET USER MEMORY "${node.fields.key || 'key'}" TO ${node.fields.variable || 'value'}\n`; break; case 'SWITCH': basCode += `SWITCH ${node.fields.expression || 'value'}\n`; break; } }); return basCode; } function exportToBas() { let basCode = "' Generated by General Bots Designer\n"; basCode += "' " + new Date().toISOString() + "\n\n"; const sortedNodes = Array.from(state.nodes.values()).sort((a, b) => { if (Math.abs(a.y - b.y) < 30) return a.x - b.x; return a.y - b.y; }); sortedNodes.forEach(node => { const template = nodeTemplates[node.type]; switch (node.type) { case 'TALK': basCode += `TALK "${node.fields.message}"\n`; break; case 'HEAR': basCode += `HEAR ${node.fields.variable} AS ${node.fields.type}\n`; break; case 'SET': basCode += `SET ${node.fields.variable} = ${node.fields.expression}\n`; break; case 'IF': basCode += `IF ${node.fields.condition} THEN\n`; break; case 'FOR': basCode += `FOR EACH ${node.fields.variable} IN ${node.fields.collection}\n`; break; case 'CALL': basCode += `CALL ${node.fields.procedure}(${node.fields.arguments})\n`; break; case 'SEND MAIL': basCode += `SEND MAIL TO "${node.fields.to}" SUBJECT "${node.fields.subject}" BODY "${node.fields.body}"\n`; break; case 'GET': basCode += `GET ${node.fields.url} TO ${node.fields.variable}\n`; break; case 'POST': basCode += `POST ${node.fields.url} WITH ${node.fields.body} TO ${node.fields.variable}\n`; break; case 'SAVE': basCode += `SAVE ${node.fields.data} TO "${node.fields.filename}"\n`; break; case 'WAIT': basCode += `WAIT ${node.fields.duration}\n`; break; case 'SET BOT MEMORY': basCode += `SET BOT MEMORY "${node.fields.key}", ${node.fields.value}\n`; break; case 'GET BOT MEMORY': basCode += `GET BOT MEMORY "${node.fields.key}" AS ${node.fields.variable}\n`; break; case 'SET USER MEMORY': basCode += `SET USER MEMORY "${node.fields.key}", ${node.fields.value}\n`; break; case 'GET USER MEMORY': basCode += `GET USER MEMORY "${node.fields.key}" AS ${node.fields.variable}\n`; break; } }); const blob = new Blob([basCode], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = (document.getElementById('current-filename').value || 'dialog') + '.bas'; a.click(); URL.revokeObjectURL(url); } function newDesign() { if (state.nodes.size > 0) { if (!confirm('Clear current design? Unsaved changes will be lost.')) return; } document.getElementById('canvas-inner').innerHTML = ''; state.nodes.clear(); state.connections = []; state.selectedNode = null; state.history = []; state.historyIndex = -1; state.nextNodeId = 1; document.getElementById('current-filename').value = ''; document.getElementById('file-name').textContent = 'Untitled'; updateConnections(); updatePropertiesPanel(); updateStatusBar(); } document.addEventListener('click', (e) => { const fileItem = e.target.closest('.file-item'); if (fileItem) { document.querySelectorAll('.file-item').forEach(f => f.classList.remove('selected')); fileItem.classList.add('selected'); document.getElementById('selected-file').value = fileItem.dataset.path; } }); async function loadFromUrlParams() { let bucket = null; let path = null; const queryParams = new URLSearchParams(window.location.search); bucket = queryParams.get('bucket'); path = queryParams.get('path'); if (!bucket || !path) { const hash = window.location.hash; const hashQueryIndex = hash.indexOf('?'); if (hashQueryIndex !== -1) { const hashParams = new URLSearchParams(hash.substring(hashQueryIndex + 1)); bucket = bucket || hashParams.get('bucket'); path = path || hashParams.get('path'); } } console.log('loadFromUrlParams called:', { bucket, path, hash: window.location.hash, search: window.location.search }); if (bucket && path) { const fileName = path.split('/').pop() || 'dialog.bas'; document.getElementById('current-filename').value = path; document.getElementById('selected-file').value = path; state.driveSource = { bucket, path }; try { const response = await fetch('/api/files/read', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bucket, path }) }); if (!response.ok) { throw new Error(`Failed to load file: ${response.statusText}`); } const data = await response.json(); const content = data.content || ''; console.log('Loaded .bas content:', content.substring(0, 200) + '...'); parseBasicCodeToNodes(content); updateStatusBar(); const statusEl = document.querySelector('.status-item span'); if (statusEl) { statusEl.textContent = `Loaded: ${fileName}`; } } catch (err) { console.error('Failed to load .bas file:', err); alert(`Failed to load file: ${err.message}`); } } } function parseBasicCodeToNodes(content) { console.log('parseBasicCodeToNodes called'); state.nodes.clear(); state.connections = []; state.nextNodeId = 1; const lines = content.split('\n'); let yPos = 100; let nodeCount = 0; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("'")) continue; const upper = trimmed.toUpperCase(); let nodeType = null; let fields = {}; if (upper.startsWith('TALK ')) { nodeType = 'TALK'; const match = trimmed.match(/TALK\s+"([^"]*)"/i) || trimmed.match(/TALK\s+(.+)/i); fields.message = match ? match[1] : ''; } else if (upper.startsWith('HEAR ')) { nodeType = 'HEAR'; const match = trimmed.match(/HEAR\s+(\w+)(?:\s+AS\s+(\w+))?/i); fields.variable = match ? match[1] : 'input'; fields.type = match && match[2] ? match[2] : 'string'; } else if (upper.startsWith('SET ') || upper.includes(' = ')) { nodeType = 'SET'; const match = trimmed.match(/(?:SET\s+)?(\w+)\s*=\s*(.+)/i); fields.variable = match ? match[1] : 'x'; fields.expression = match ? match[2] : '0'; } else if (upper.startsWith('IF ')) { nodeType = 'IF'; const match = trimmed.match(/IF\s+(.+?)\s+THEN/i); fields.condition = match ? match[1] : 'true'; } else if (upper.startsWith('FOR ')) { nodeType = 'FOR'; const match = trimmed.match(/FOR\s+(?:EACH\s+)?(\w+)\s+IN\s+(.+)/i); fields.variable = match ? match[1] : 'item'; fields.collection = match ? match[2] : 'items'; } else if (upper.startsWith('CALL ')) { nodeType = 'CALL'; const match = trimmed.match(/CALL\s+(\w+)\s*\(([^)]*)\)/i); fields.procedure = match ? match[1] : 'sub'; fields.arguments = match ? match[2] : ''; } else if (upper.startsWith('WAIT ')) { nodeType = 'WAIT'; const match = trimmed.match(/WAIT\s+(\d+)/i); fields.duration = match ? match[1] : '1000'; } else if (upper.startsWith('GET ')) { nodeType = 'GET'; const match = trimmed.match(/GET\s+(.+?)\s+TO\s+(\w+)/i); fields.url = match ? match[1] : ''; fields.variable = match ? match[2] : 'result'; } else if (upper.startsWith('PARAM ')) { nodeType = 'HEAR'; const match = trimmed.match(/PARAM\s+(\w+)\s+AS\s+(\w+)/i); fields.variable = match ? match[1] : 'param'; fields.type = match ? match[2] : 'string'; } if (nodeType && nodeTemplates[nodeType]) { const node = createNode(nodeType, 400, yPos); if (node) { Object.assign(node.fields, fields); const nodeEl = document.getElementById(node.id); if (nodeEl) { nodeEl.querySelectorAll('.node-field-input, .node-field-select, textarea').forEach(input => { const fieldName = input.dataset.field || input.name; if (fields[fieldName] !== undefined) { input.value = fields[fieldName]; } }); } yPos += 100; nodeCount++; console.log('Created node:', nodeType, fields); } } } console.log(`Parsed ${nodeCount} nodes from BASIC code`); updateStatusBar(); saveToHistory(); } function initializeCanvas() { console.log('initializeCanvas called'); const canvasLoaded = document.querySelector('.canvas-loaded'); if (!canvasLoaded) { console.log('No canvas-loaded element found'); return; } const content = canvasLoaded.dataset.content || ''; console.log('Canvas content from server:', content.substring(0, 100)); canvasLoaded.remove(); parseBasicCodeToNodes(content); }