@hhoangphuoc/escape-room-cli
Version:
A CLI for playing AI-generated escape room games. Install globally with: npm install -g @hhoangphuoc/escape-room-cli
270 lines (269 loc) • 16.5 kB
JavaScript
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { useState, useEffect, useRef } from 'react';
import Spinner from 'ink-spinner';
import { Box, Text, useInput } from 'ink';
import CommandInput from './CommandInput.js';
import CommandHistory from './CommandHistory.js';
import ScrollableBox from './ScrollableBox.js';
import ModelSelector from './ModelSelector.js';
import Instructions from './Instructions.js';
import Leaderboard from './Leaderboard.js';
import CostMonitor from './CostMonitor.js';
import CostDashboard from './CostDashboard.js';
// import {BudgetAlerts} from './BudgetAlert.js'; //TODO: NOT YET IMPLEMETED
import { getApiUrl } from '../utils/apiConfig.js';
import * as commandProcessor from '../utils/commandProcessor.js';
import { useAuth } from '../hooks/useAuth.js';
const Terminal = ({ autoLogin, onTriggerRegister }) => {
const { auth, login, logout } = useAuth();
const { userId, sessionToken, userName, apiKey } = auth;
// State for UI
const [history, setHistory] = useState([]);
const [showHistory, setShowHistory] = useState(false);
// ------------------------------------------------------------------------------------------------
// LOADING STATES
// -------------------------------------------------------------------------------------------------
// isLoadingGame: states if the game is loading
// isProcessingCommand: states if the command is being processed by AI - Should be different from `isLoadingGame`
// loadingMessage: message update for both cases
const [isLoadingGame, setIsLoadingGame] = useState(false);
const [isProcessingCommand, setIsProcessingCommand] = useState(false);
const [loadingMessage, setLoadingMessage] = useState('');
// ------------------------------------------------------------------------------------------------
const [showModelSelector, setShowModelSelector] = useState(false);
const [showInstructions, setShowInstructions] = useState(false);
const [showLeaderboard, setShowLeaderboard] = useState(false);
const [leaderboardData, setLeaderboardData] = useState(null);
const [showCostDashboard, setShowCostDashboard] = useState(false);
const [costDashboardData, setCostDashboardData] = useState(null);
const [showCostMonitor, setShowCostMonitor] = useState(false);
const [costMonitorData, setCostMonitorData] = useState(null);
const [budgetAlerts, setBudgetAlerts] = useState([]);
const [mcpMode, setMcpMode] = useState(false);
const [command, setCommand] = useState('');
// State for Game
const [currentGameId, setCurrentGameId] = useState(null);
const [currentRoomName, setCurrentRoomName] = useState('Welcome to the AI Escape Room!');
const [currentRoomBackground, setCurrentRoomBackground] = useState('Type /register or /login to start.');
const [currentRoomObjects, setCurrentRoomObjects] = useState([]);
const [unlockedObjects, setUnlockedObjects] = useState([]);
const [currentGameMode, setCurrentGameMode] = useState('unknown');
const [totalRooms, setTotalRooms] = useState(0);
// AI Model State
const [selectedModel, setSelectedModel] = useState({
value: 'gpt-4.1-mini',
label: 'GPT-4.1-mini',
});
const hasAICapability = Boolean(auth.apiKey ||
process.env['OPENAI_API_KEY'] ||
process.env['ANTHROPIC_API_KEY']);
const historyRef = useRef(null);
// Handle ESC key to close overlays
useInput((_, key) => {
if (key.escape) {
if (showInstructions) {
setShowInstructions(false);
}
else if (showLeaderboard) {
setShowLeaderboard(false);
setLeaderboardData(null);
}
else if (showCostDashboard) {
setShowCostDashboard(false);
setCostDashboardData(null);
}
else if (showCostMonitor) {
setShowCostMonitor(false);
setCostMonitorData(null);
}
else if (showModelSelector) {
setShowModelSelector(false);
}
else if (budgetAlerts.length > 0) {
setBudgetAlerts([]);
}
}
});
useEffect(() => {
if (historyRef.current) {
historyRef.current.scrollToBottom();
}
}, [history]);
useEffect(() => {
if (autoLogin) {
handleCommand('/login');
}
}, [autoLogin]);
useEffect(() => {
if (sessionToken) {
fetchGameState();
}
}, [sessionToken]);
const fetchGameState = async () => {
if (!sessionToken)
return;
const apiUrl = getApiUrl();
setIsLoadingGame(true);
setLoadingMessage('Loading game state...');
try {
const response = await fetch(`${apiUrl}/api/game/state`, {
headers: { Authorization: `Bearer ${sessionToken}` },
});
if (response.ok) {
const data = (await response.json());
if (data.game) {
setCurrentGameId(data.game.id);
setCurrentRoomName(data.game.roomName);
setCurrentRoomBackground(data.game.background || 'You are in a room.');
setCurrentGameMode(data.game.mode || 'unknown');
setTotalRooms(data.game.totalRooms || 0);
setCurrentRoomObjects(data.game.objects || []);
setUnlockedObjects(data.game.unlockedObjects || []);
}
else {
setCurrentRoomBackground('No active game found. Use /newgame to start.');
}
}
else {
setCurrentRoomBackground('Could not fetch game state. Your session might be invalid. Try /logout and /login again.');
}
}
catch (error) {
setCurrentRoomBackground('Network error fetching game state.');
}
finally {
setIsLoadingGame(false);
setLoadingMessage('');
}
};
// HANDLE MCP COMMANDS ----------------------------------------------------------------------------
const handleMcpCommand = async (commandValue) => {
if (commandValue === '/mcp') {
setHistory(prev => [
...prev,
{ type: 'response', text: 'Switching to MCP mode...' },
]);
setMcpMode(true);
return 'MCP mode activated. Type /help for help.';
}
return 'MCP command processing not implemented yet. Type /help for help.';
};
// ------------------------------------------------------------------------------------------------
// ================================================================================================================================
// MAIN COMMAND HANDLER
// ================================================================================================================================
async function handleCommand(commandValue) {
if (showModelSelector || showInstructions || showLeaderboard)
return;
setHistory(prev => [...prev, { type: 'command', text: commandValue }]);
if (commandValue !== '/history')
setShowHistory(false);
if (commandValue === '/register') {
onTriggerRegister();
return;
}
if (commandValue === '/mcp') {
setHistory(prev => [
...prev,
{ type: 'response', text: 'Switching to MCP mode...' },
]);
setMcpMode(true);
return;
}
if (mcpMode) {
const response = await handleMcpCommand(commandValue);
setHistory(prev => [...prev, { type: 'response', text: response }]);
}
else {
await commandProcessor.handleCommand({
command: commandValue,
auth: {
userId,
sessionToken,
userName,
apiKey,
login,
logout,
},
game: {
gameId: currentGameId,
roomName: currentRoomName,
roomBackground: currentRoomBackground,
gameMode: currentGameMode,
totalRooms: totalRooms,
unlockedObjects: unlockedObjects,
currentRoomObjects: currentRoomObjects,
setGame: (gameData) => {
setCurrentGameId(gameData.id);
setCurrentRoomName(gameData.name);
setCurrentRoomBackground(gameData.background);
setCurrentGameMode(gameData.mode === 'multi-room' ? 'multi-room' : 'single-room');
setTotalRooms(gameData.totalRooms);
},
setGameFromLook: (roomData) => {
setCurrentRoomName(roomData.name);
setCurrentRoomObjects(roomData.objects);
if (roomData.background)
setCurrentRoomBackground(roomData.background);
},
unlockObject: (objectName) => {
setUnlockedObjects(prev => [...prev, objectName]);
},
setGameCompleted: () => {
setCurrentRoomName('Congratulations!');
setCurrentRoomBackground('Game Completed. You can start try out with a new game [/newgame] or [/logout] to end your session.');
setCurrentGameId(null);
},
resetGame: () => {
setCurrentGameId(null);
setCurrentRoomName('Game Reset');
setCurrentRoomBackground('Use /newgame to start a new game.');
setCurrentGameMode('unknown');
setTotalRooms(0);
setUnlockedObjects([]);
setCurrentRoomObjects([]);
},
fetchGameState: fetchGameState,
},
setHistory,
setIsLoading: setIsProcessingCommand,
setLoadingMessage,
setShowHistory,
setShowModelSelector,
setShowInstructions,
setShowLeaderboard: (data) => {
setLeaderboardData(data);
setShowLeaderboard(true);
},
setShowCostDashboard: (data) => {
if (data)
setCostDashboardData(data);
setShowCostDashboard(true);
},
setShowCostMonitor: (data) => {
if (data)
setCostMonitorData(data);
setShowCostMonitor(true);
},
selectedModel,
});
}
}
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsxs(Box, { marginY: 1, justifyContent: "space-between", padding: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: mcpMode ? 'magenta' : 'cyan', children: [userName ? `${userName}'s Escape Room Game` : 'Escape Room', " [", mcpMode ? 'MCP' : `${currentGameMode || 'default'}`, "]"] }), mcpMode && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: "cyan", children: "MCP Help: /help" }) })), hasAICapability && !mcpMode && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 AI available: ", selectedModel?.label] }) }))] }), _jsxs(Box, { flexDirection: "column", alignItems: "flex-end", children: [_jsx(Box, { children: _jsx(Text, { color: isLoadingGame || !!sessionToken ? 'green' : 'red', children: isLoadingGame || !!sessionToken ? '● Online' : '◌ Offline' }) }), auth.costTracking && sessionToken && hasAICapability && (_jsx(Box, { marginTop: 1, children: _jsx(CostMonitor, { costData: {
currentSessionCost: auth.costTracking.currentSessionCost,
currentSessionTokens: auth.costTracking.currentSessionTokens,
userTotalCost: auth.costTracking.totalCost,
userTotalTokens: auth.costTracking.totalTokens,
lastRequestCost: auth.costTracking.lastRequestCost,
lastRequestTokens: auth.costTracking.lastRequestTokens,
averageCostPerRequest: 0,
modelUsageBreakdown: {}
}, compact: true, showTokens: false }) }))] })] }), _jsx(Box, { padding: 1, flexDirection: "column", children: _jsx(ScrollableBox, { ref: historyRef, children: _jsx(CommandHistory, { history: showHistory ? history : history.slice(-2), showHistory: showHistory }) }) }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginY: 1, children: _jsx(CommandInput, { value: command, onChange: setCommand, onSubmit: handleCommand, mode: mcpMode ? 'mcp' : 'standard', currentRoomObjects: currentRoomObjects }) }), showModelSelector && (_jsx(ModelSelector, { onSelect: (model) => {
setSelectedModel(model);
setHistory(prev => [
...prev,
{ type: 'response', text: `Model set to ${model.label}` },
]);
}, onClose: () => setShowModelSelector(false) })), showInstructions && (_jsx(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1, width: "100%", height: "100%", children: _jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsx(Instructions, {}), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { color: "gray", children: "Press ESC to close" }) })] }) })), showLeaderboard && leaderboardData && (_jsx(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1, width: "100%", height: "100%", children: _jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsx(Leaderboard, { entries: leaderboardData.entries, count: leaderboardData.count, mode: leaderboardData.mode, showCostMetrics: leaderboardData.showCostMetrics }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { color: "gray", children: "Press ESC to close" }) })] }) })), showCostDashboard && (_jsx(Box, { borderStyle: "round", borderColor: "green", padding: 1, marginBottom: 1, width: "100%", height: "100%", children: _jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [costDashboardData ? (_jsx(CostDashboard, { usageData: costDashboardData })) : (_jsx(Box, { padding: 2, justifyContent: "center", children: _jsx(Text, { color: "yellow", children: "Loading cost dashboard..." }) })), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { color: "gray", children: "Press ESC to close" }) })] }) })), showCostMonitor && (_jsx(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1, width: "100%", height: "100%", children: _jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [costMonitorData ? (_jsx(CostMonitor, { costData: costMonitorData })) : (_jsx(Box, { padding: 2, justifyContent: "center", children: _jsx(Text, { color: "yellow", children: "Loading cost monitor..." }) })), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { color: "gray", children: "Press ESC to close" }) })] }) })), (isProcessingCommand || isLoadingGame) && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, rowGap: 0.5, paddingY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " ", loadingMessage] }) }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: "For interactive game commands, type /help to see available commands." }) })] }))] }));
};
export default Terminal;