rts-commander / static /nl_interface.js
Luigi's picture
fix: Shared LLM model manager and UI positioning
1e98ab1
raw
history blame
15.6 kB
/**
* 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);
});