generalbots/botui/ui/suite/designer/designer-io.js

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);
}