/** * Natural Language Command Interface for RTS Game * Allows players to control the game using natural language in EN/FR/ZH */ class NLInterface { constructor(gameClient) { this.gameClient = gameClient; this.translator = null; this.translatorReady = false; this.lastCommand = null; this.commandHistory = []; this.maxHistorySize = 50; this.initializeUI(); this.checkTranslatorStatus(); } initializeUI() { // Create NL interface container const nlContainer = document.createElement('div'); nlContainer.id = 'nl-interface'; nlContainer.className = 'nl-interface'; nlContainer.innerHTML = `
🎤 Natural Language Commands
Checking translator...
📝 Example Commands:
Loading examples...
`; // Insert into left sidebar (after existing sections) const leftSidebar = document.getElementById('left-sidebar'); if (leftSidebar) { leftSidebar.appendChild(nlContainer); } else { // Fallback: insert before right sidebar if left sidebar not found const rightSidebar = document.getElementById('right-sidebar'); if (rightSidebar) { rightSidebar.parentElement.insertBefore(nlContainer, rightSidebar); } else { document.getElementById('game-container').appendChild(nlContainer); } } this.setupEventListeners(); } setupEventListeners() { // Toggle button const toggleBtn = document.getElementById('nl-toggle'); const nlBody = document.getElementById('nl-body'); if (toggleBtn && nlBody) { toggleBtn.addEventListener('click', () => { nlBody.classList.toggle('collapsed'); const icon = toggleBtn.querySelector('.toggle-icon'); icon.textContent = nlBody.classList.contains('collapsed') ? '▶' : '▼'; }); } // Input field const nlInput = document.getElementById('nl-input'); if (nlInput) { nlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !nlInput.disabled) { this.sendCommand(); } }); } // Send button const sendBtn = document.getElementById('nl-send'); if (sendBtn) { sendBtn.addEventListener('click', () => this.sendCommand()); } // Confirmation buttons const executeBtn = document.getElementById('confirm-execute'); const cancelBtn = document.getElementById('confirm-cancel'); if (executeBtn) { executeBtn.addEventListener('click', () => this.executeCommand()); } if (cancelBtn) { cancelBtn.addEventListener('click', () => this.hideConfirmation()); } } async checkTranslatorStatus() { try { const response = await fetch('/api/nl/status'); const data = await response.json(); if (data.available) { this.translatorReady = true; this.updateStatus('ready', 'Ready'); this.enableInput(); await this.loadExamples(); } else { this.translatorReady = false; this.updateStatus('error', data.last_error || 'Translator not available'); this.disableInput(); } } catch (error) { console.error('[NL] Failed to check translator status:', error); this.updateStatus('error', 'Failed to connect to translator'); this.disableInput(); } } async loadExamples() { try { const lang = this.gameClient.currentLanguage || 'en'; const response = await fetch(`/api/nl/examples?language=${lang}`); const data = await response.json(); const examplesList = document.getElementById('examples-list'); if (examplesList && data.examples) { examplesList.innerHTML = data.examples .map(cmd => `
💬 ${cmd}
`) .join(''); // Add click handlers examplesList.querySelectorAll('.example-item').forEach(item => { item.addEventListener('click', () => { const cmd = item.getAttribute('data-command'); document.getElementById('nl-input').value = cmd; }); }); } } catch (error) { console.error('[NL] Failed to load examples:', error); } } updateStatus(state, text) { const statusDot = document.querySelector('#nl-status .status-dot'); const statusText = document.getElementById('nl-status-text'); if (statusDot) { statusDot.className = 'status-dot'; statusDot.classList.add(state); } if (statusText) { statusText.textContent = text; } } enableInput() { const nlInput = document.getElementById('nl-input'); const sendBtn = document.getElementById('nl-send'); if (nlInput) nlInput.disabled = false; if (sendBtn) sendBtn.disabled = false; } disableInput() { const nlInput = document.getElementById('nl-input'); const sendBtn = document.getElementById('nl-send'); if (nlInput) nlInput.disabled = true; if (sendBtn) sendBtn.disabled = true; } async sendCommand() { const nlInput = document.getElementById('nl-input'); const command = nlInput.value.trim(); if (!command) { return; } // Update status this.updateStatus('loading', 'Translating...'); this.disableInput(); try { const response = await fetch('/api/nl/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: command, language: this.gameClient.currentLanguage }) }); const result = await response.json(); if (result.success && result.json_command) { // Show confirmation this.showConfirmation(command, result.json_command); this.updateStatus('ready', `Ready (${result.response_time.toFixed(2)}s)`); // Add to history this.addToHistory(command, result.json_command); } else { // Show error const errorMsg = result.error || 'Failed to translate command'; this.updateStatus('error', errorMsg); this.gameClient.showNotification(`NL Error: ${errorMsg}`, 'error'); setTimeout(() => { this.updateStatus('ready', 'Ready'); this.enableInput(); }, 3000); } } catch (error) { console.error('[NL] Translation error:', error); this.updateStatus('error', 'Network error'); this.gameClient.showNotification('Failed to translate command', 'error'); setTimeout(() => { this.updateStatus('ready', 'Ready'); this.enableInput(); }, 3000); } } showConfirmation(originalCommand, jsonCommand) { const confirmation = document.getElementById('nl-confirmation'); const toolSpan = document.getElementById('confirm-tool'); const paramsSpan = document.getElementById('confirm-params'); if (confirmation && toolSpan && paramsSpan) { // Store for execution this.lastCommand = { original: originalCommand, json: jsonCommand }; // Display toolSpan.textContent = jsonCommand.tool; paramsSpan.textContent = jsonCommand.params ? JSON.stringify(jsonCommand.params, null, 2) : 'None'; confirmation.style.display = 'block'; } } hideConfirmation() { const confirmation = document.getElementById('nl-confirmation'); if (confirmation) { confirmation.style.display = 'none'; } this.lastCommand = null; this.enableInput(); // Clear input const nlInput = document.getElementById('nl-input'); if (nlInput) { nlInput.value = ''; } } async executeCommand() { if (!this.lastCommand) { return; } const { json } = this.lastCommand; // Execute via MCP or direct game command try { // Convert MCP command to game command const gameCommand = this.convertMCPToGameCommand(json); if (gameCommand) { // Send via WebSocket this.gameClient.ws.send(JSON.stringify(gameCommand)); this.gameClient.showNotification('Command executed', 'success'); } else { this.gameClient.showNotification('Unsupported command', 'error'); } } catch (error) { console.error('[NL] Execution error:', error); this.gameClient.showNotification('Failed to execute command', 'error'); } this.hideConfirmation(); } convertMCPToGameCommand(mcpCommand) { const { tool, params } = mcpCommand; // Map MCP tools to game commands switch (tool) { case 'get_game_state': // This just shows notification with game state const state = this.gameClient.gameState; if (state) { const player = state.players[0]; const msg = `Credits: ${player.credits} | Power: ${player.power}/${player.power_max} | Units: ${Object.keys(state.units).length}`; this.gameClient.showNotification(msg, 'info'); } return null; case 'move_units': // Move selected units or units by ID if (params && params.target_x !== undefined && params.target_y !== undefined) { return { type: 'move_units', player_id: 0, unit_ids: params.unit_ids || Array.from(this.gameClient.selectedUnits), x: params.target_x, y: params.target_y }; } break; case 'attack_unit': // Attack target if (params && params.target_id) { return { type: 'attack', player_id: 0, attacker_ids: params.attacker_ids || Array.from(this.gameClient.selectedUnits), target_id: params.target_id }; } break; case 'build_unit': // Build unit if (params && params.unit_type) { return { type: 'build_unit', player_id: 0, unit_type: params.unit_type, building_id: this.gameClient.selectedProductionBuilding || null }; } break; case 'build_building': // Build building if (params && params.building_type && params.x !== undefined && params.y !== undefined) { return { type: 'build_building', player_id: 0, building_type: params.building_type, x: params.x, y: params.y }; } break; } return null; } addToHistory(command, jsonCommand) { this.commandHistory.unshift({ timestamp: Date.now(), command: command, json: jsonCommand }); if (this.commandHistory.length > this.maxHistorySize) { this.commandHistory.pop(); } } } // Initialize NL interface when game is ready window.addEventListener('load', () => { // Wait for game client to be ready const checkGameReady = setInterval(() => { if (window.gameClient && window.gameClient.ws) { clearInterval(checkGameReady); // Initialize NL interface window.nlInterface = new NLInterface(window.gameClient); console.log('[NL] Interface initialized'); } }, 100); });