377 lines
12 KiB
JavaScript
377 lines
12 KiB
JavaScript
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);
|
|
}
|