// VBot Control Logic
const API_BASE = '/api/v1';
let selectedVBotId = null;
// DOM Elements
const vbotList = document.getElementById('vbot-list');
const createBtn = document.getElementById('create-vbot-btn');
const refreshBtn = document.getElementById('refresh-btn');
const controlPanel = document.getElementById('vbot-control-panel');
const connectionStatus = document.getElementById('connection-status');
// Init
document.addEventListener('DOMContentLoaded', () => {
checkHealth();
loadVBots();
// Event Listeners
refreshBtn.addEventListener('click', loadVBots);
createBtn.addEventListener('click', createVBot);
// Movement Controls
setupMovementControls();
});
async function checkHealth() {
try {
const response = await fetch(`${API_BASE}/health`);
if (response.ok) {
connectionStatus.textContent = '🟢 Connected';
connectionStatus.className = 'status-connected';
connectionStatus.style.color = '#4caf50';
} else {
throw new Error('Health check failed');
}
} catch (e) {
connectionStatus.textContent = '🔴 Disconnected';
connectionStatus.className = 'status-disconnected';
connectionStatus.style.color = '#f44336';
}
}
async function loadVBots() {
vbotList.innerHTML = '<div style="text-align: center; padding: 10px;">Loading...</div>';
try {
// We use the same robots endpoint, but filter/highlight virtual ones
const response = await fetch(`${API_BASE}/robots`);
const data = await response.json();
const robots = data.robots || [];
// Filter for virtual robots (or show all with indicators)
// For VBot page, we primarily want virtual ones, but 'robotics-mcp' treats them similarly
// We'll calculate which are virtual based on platform/type or metadata
const vbots = robots.filter(r => r.platform === 'unity' || r.platform === 'pixels' || r.id.startsWith('vbot'));
renderVBotList(vbots);
} catch (e) {
vbotList.innerHTML = `<div style="color: red; text-align: center;">Error: ${e.message}</div>`;
}
}
function renderVBotList(bots) {
vbotList.innerHTML = '';
if (bots.length === 0) {
vbotList.innerHTML = '<div style="text-align: center; padding: 15px; color: #888;">No active VBots found. Create one!</div>';
return;
}
bots.forEach(bot => {
const item = document.createElement('div');
item.className = `vbot-list-item ${selectedVBotId === bot.id ? 'selected' : ''}`;
item.onclick = () => selectVBot(bot);
item.innerHTML = `
<div>
<strong>${bot.id}</strong> <small style="color: #aaa;">(${bot.type})</small>
</div>
<div style="font-size: 0.8em; background: #444; padding: 2px 6px; border-radius: 4px;">
${bot.platform || 'virtual'}
</div>
<button onclick="deleteVBot(event, '${bot.id}')" style="background: none; border: none; cursor: pointer; color: #f44336; margin-left: 10px;">🗑️</button>
`;
vbotList.appendChild(item);
});
}
async function createVBot() {
const id = document.getElementById('new-vbot-id').value.trim();
const type = document.getElementById('new-vbot-type').value;
const platform = document.getElementById('new-vbot-platform').value;
if (!id) {
alert('Please enter a VBot ID');
return;
}
try {
const response = await fetch(`${API_BASE}/robots`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
robot_id: id,
robot_type: type,
platform: platform,
metadata: { created_via: 'web_ui' }
})
});
if (response.ok) {
document.getElementById('new-vbot-id').value = '';
loadVBots();
} else {
const err = await response.json();
alert(`Failed: ${err.detail}`);
}
} catch (e) {
alert(`Error: ${e.message}`);
}
}
async function deleteVBot(e, id) {
e.stopPropagation();
if (!confirm(`Delete VBot ${id}?`)) return;
try {
await fetch(`${API_BASE}/robots/${id}`, { method: 'DELETE' });
if (selectedVBotId === id) {
selectedVBotId = null;
controlPanel.style.display = 'none';
}
loadVBots();
} catch (e) {
alert(`Error: ${e.message}`);
}
}
function selectVBot(bot) {
selectedVBotId = bot.id;
controlPanel.style.display = 'block';
// Update selection UI
document.querySelectorAll('.vbot-list-item').forEach(el => el.classList.remove('selected'));
// Re-render to show selection state (lazy way)
// Or just find the element:
// This part is handled by re-render usually, but let's just update header
document.getElementById('current-vbot-id').textContent = bot.id;
document.getElementById('vbot-type').textContent = bot.type;
document.getElementById('vbot-platform').textContent = bot.platform || 'virtual';
updateVBotStatus();
}
async function updateVBotStatus() {
if (!selectedVBotId) return;
try {
const response = await fetch(`${API_BASE}/robots/${selectedVBotId}/status`);
if (response.ok) {
const status = await response.json();
// Update UI
document.getElementById('vbot-type').textContent = status.robot_type || status.type || '-';
document.getElementById('vbot-platform').textContent = status.platform || '-';
if (status.position) {
const pos = status.position;
document.getElementById('vbot-position').textContent =
`x:${pos.x?.toFixed(2)}, y:${pos.y?.toFixed(2)}, z:${pos.z?.toFixed(2)}`;
}
if (status.rotation) {
const rot = status.rotation;
// Handle different rotation formats (euler vs quat)
// Assuming euler for display if available, else simple string
document.getElementById('vbot-rotation').textContent =
JSON.stringify(rot);
}
}
} catch (e) {
console.error('Status update failed', e);
}
}
// Polling
setInterval(() => {
checkHealth();
if (selectedVBotId) {
updateVBotStatus();
}
}, 3000); // Poll every 3 seconds for VBot responsiveness
// Controls
function setupMovementControls() {
const actions = {
'move-fwd': { action: 'move', linear: 0.5, angular: 0.0 },
'move-back': { action: 'move', linear: -0.5, angular: 0.0 },
'move-left': { action: 'move', linear: 0.0, angular: 0.5 }, // Turn logic might vary
'move-right': { action: 'move', linear: 0.0, angular: -0.5 },
'stop-move': { action: 'stop' },
'anim-wave': { action: 'emote', name: 'wave' },
'anim-dance': { action: 'emote', name: 'dance' },
'anim-sit': { action: 'posture', name: 'sit' },
'anim-stand': { action: 'posture', name: 'stand' },
'respawn': { action: 'respawn' }
};
for (const [id, cmd] of Object.entries(actions)) {
const btn = document.getElementById(id);
if (btn) {
btn.addEventListener('click', () => sendCommand(cmd));
}
}
// Custom anim
document.getElementById('play-custom-anim').addEventListener('click', () => {
const name = document.getElementById('custom-anim').value;
if (name) sendCommand({ action: 'emote', name });
});
}
async function sendCommand(payload) {
if (!selectedVBotId) return;
try {
await fetch(`${API_BASE}/robots/${selectedVBotId}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (e) {
console.error('Command failed', e);
}
}