UNPKG

@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
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;