Spaces:
Sleeping
Sleeping
| /** | |
| * 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 = ` | |
| <div class="nl-header"> | |
| <span class="nl-icon">🎤</span> | |
| <span class="nl-title">Natural Language Commands</span> | |
| <button id="nl-toggle" class="nl-toggle-btn" title="Toggle NL Interface"> | |
| <span class="toggle-icon">▼</span> | |
| </button> | |
| </div> | |
| <div class="nl-body" id="nl-body"> | |
| <div class="nl-status" id="nl-status"> | |
| <span class="status-dot loading"></span> | |
| <span id="nl-status-text">Checking translator...</span> | |
| </div> | |
| <div class="nl-input-container"> | |
| <input type="text" | |
| id="nl-input" | |
| class="nl-input" | |
| placeholder="Type command in English, French, or Chinese..." | |
| disabled> | |
| <button id="nl-send" class="nl-send-btn" disabled> | |
| <span>Send</span> | |
| </button> | |
| </div> | |
| <div class="nl-examples" id="nl-examples"> | |
| <div class="examples-header">📝 Example Commands:</div> | |
| <div class="examples-list" id="examples-list"> | |
| Loading examples... | |
| </div> | |
| </div> | |
| <div class="nl-confirmation" id="nl-confirmation" style="display: none;"> | |
| <div class="confirmation-header">Parsed Command:</div> | |
| <div class="confirmation-content"> | |
| <div class="confirmation-tool"> | |
| <strong>Action:</strong> <span id="confirm-tool"></span> | |
| </div> | |
| <div class="confirmation-params"> | |
| <strong>Parameters:</strong> | |
| <pre id="confirm-params"></pre> | |
| </div> | |
| </div> | |
| <div class="confirmation-buttons"> | |
| <button id="confirm-execute" class="confirm-btn execute">Execute</button> | |
| <button id="confirm-cancel" class="confirm-btn cancel">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // 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 => `<div class="example-item" data-command="${cmd}">💬 ${cmd}</div>`) | |
| .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); | |
| }); | |