dlouapre's picture
dlouapre HF Staff
Initial
ee6161a
raw
history blame
8.69 kB
// RMScript App - Frontend Application
// Connects to the settings_app backend for compilation and server-side execution
// API is served from same origin (settings_app)
const API_BASE = '';
// Global state
const state = {
currentIR: null,
isExecuting: false
};
// DOM elements
const elements = {
editor: document.getElementById('editor'),
compileStatus: document.getElementById('compileStatus'),
robotStatus: document.getElementById('robotStatus'),
console: document.getElementById('console'),
irDisplay: document.getElementById('irDisplay'),
executionInfo: document.getElementById('executionInfo'),
executeBtn: document.getElementById('executeBtn')
};
// Example scripts
const examples = {
basic: `"Simple head movement"
look left
wait 1s
look right
wait 1s
look center`,
complex: `"Wave hello"
look left
antenna both up
wait 1s
look right
antenna both down
wait 0.5s
look center`,
repeat: `"Repeat demo"
REPEAT 3
look left
wait 0.5s
look right
wait 0.5s
END
look center`
};
// Update compile status UI
function updateCompileStatus(status, message) {
const dot = status === 'success' ? 'green' : status === 'error' ? 'red' : 'gray';
elements.compileStatus.className = `status ${status}`;
elements.compileStatus.innerHTML = `<span><span class="status-dot ${dot}"></span>${message}</span>`;
}
// Log to console
function log(message, type = 'info') {
const line = document.createElement('div');
line.className = `console-line ${type}`;
line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
elements.console.appendChild(line);
elements.console.scrollTop = elements.console.scrollHeight;
}
// Clear console
function clearConsole() {
elements.console.innerHTML = '';
}
// Load example script
function loadExample(exampleName) {
if (examples[exampleName]) {
elements.editor.value = examples[exampleName];
log(`Loaded ${exampleName} example`, 'info');
}
}
// Clear editor
function clearEditor() {
elements.editor.value = '';
state.currentIR = null;
elements.irDisplay.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">No IR yet. Verify a script first!</div>';
updateCompileStatus('idle', 'Ready');
}
// Verify script (syntax and semantics only)
async function verifyScript() {
const source = elements.editor.value.trim();
if (!source) {
log('Editor is empty', 'warning');
return;
}
clearConsole();
log('Verifying script...', 'info');
updateCompileStatus('idle', 'Verifying...');
try {
const response = await fetch(`${API_BASE}/api/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source })
});
const result = await response.json();
if (result.success) {
log('Verification successful!', 'success');
if (result.name) log(` Name: ${result.name}`, 'info');
if (result.description) log(` Description: ${result.description}`, 'info');
if (result.warnings.length > 0) {
result.warnings.forEach(w => {
log(` Warning (line ${w.line}): ${w.message}`, 'warning');
});
}
updateCompileStatus('success', 'Valid script');
} else {
log('Verification failed', 'error');
result.errors.forEach(e => {
log(` Error (line ${e.line}): ${e.message}`, 'error');
});
updateCompileStatus('error', 'Invalid script');
}
} catch (error) {
log(`Backend error: ${error.message}`, 'error');
updateCompileStatus('error', 'Backend error');
}
}
// Compile script to IR
async function compileScript() {
const source = elements.editor.value.trim();
if (!source) {
log('Editor is empty', 'warning');
return null;
}
log('Compiling script to IR...', 'info');
updateCompileStatus('idle', 'Compiling...');
try {
const response = await fetch(`${API_BASE}/api/compile`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source })
});
const result = await response.json();
if (result.success) {
log(`Compiled ${result.ir.length} actions`, 'success');
if (result.warnings.length > 0) {
result.warnings.forEach(w => {
log(` Warning (line ${w.line}): ${w.message}`, 'warning');
});
}
state.currentIR = result.ir;
displayIR(result.ir);
updateCompileStatus('success', `${result.ir.length} actions ready`);
return result.ir;
} else {
log('Compilation failed', 'error');
result.errors.forEach(e => {
log(` Error (line ${e.line}): ${e.message}`, 'error');
});
updateCompileStatus('error', 'Compilation failed');
return null;
}
} catch (error) {
log(`Backend error: ${error.message}`, 'error');
updateCompileStatus('error', 'Backend error');
return null;
}
}
// Display IR in the UI
function displayIR(ir) {
if (!ir || ir.length === 0) {
elements.irDisplay.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">No actions</div>';
return;
}
const html = ir.map((action, idx) => {
let details = '';
if (action.type === 'action') {
details = `Duration: ${action.duration}s`;
if (action.head_pose) details += ', Head movement';
if (action.antennas) details += `, Antennas: [${action.antennas.map(a => a.toFixed(2)).join(', ')}]`;
if (action.body_yaw !== null) details += `, Body yaw: ${action.body_yaw.toFixed(2)}`;
} else if (action.type === 'wait') {
details = `Wait ${action.duration}s`;
} else if (action.type === 'picture') {
details = 'Take picture';
} else if (action.type === 'sound') {
details = `Play "${action.sound_name}"`;
if (action.blocking) details += ' (blocking)';
if (action.loop) details += ' (loop)';
}
return `
<div class="ir-action">
<div class="ir-action-type">${idx + 1}. ${action.type.toUpperCase()}</div>
<div class="ir-action-details">${details}</div>
</div>
`;
}).join('');
elements.irDisplay.innerHTML = html;
}
// Execute script on robot (server-side via SDK)
async function executeScript() {
if (state.isExecuting) {
log('Already executing a script', 'warning');
return;
}
// Compile the current editor content
const ir = await compileScript();
if (!ir) {
log('Cannot execute - compilation failed', 'error');
return;
}
// Execute via server-side SDK
state.isExecuting = true;
elements.executeBtn.disabled = true;
clearExecutionInfo();
logExecution('Sending to robot...', 'info');
try {
const response = await fetch(`${API_BASE}/api/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ir })
});
const result = await response.json();
if (result.success) {
logExecution(`Execution complete! ${result.actions_executed} actions executed.`, 'success');
} else {
logExecution(`Execution failed: ${result.message}`, 'error');
}
} catch (error) {
logExecution(`Execution error: ${error.message}`, 'error');
} finally {
state.isExecuting = false;
elements.executeBtn.disabled = false;
}
}
// Log execution info
function logExecution(message, type = 'info') {
const line = document.createElement('div');
line.className = `console-line ${type}`;
line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
elements.executionInfo.appendChild(line);
elements.executionInfo.scrollTop = elements.executionInfo.scrollHeight;
}
// Clear execution info
function clearExecutionInfo() {
elements.executionInfo.innerHTML = '';
}
// Initialize application
function init() {
console.log('Initializing RMScript App');
loadExample('basic');
console.log('RMScript App ready');
}
// Start when DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}