@hhoangphuoc/escape-room-cli
Version:
A CLI for playing AI-generated escape room games. Install globally with: npm install -g @hhoangphuoc/escape-room-cli
273 lines (272 loc) • 15.9 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } 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 ConversationHistoryComponent from './ConversationHistory.js';
import ScrollableBox from './ScrollableBox.js';
import ModelSelector from './ModelSelector.js';
import Leaderboard from './Leaderboard.js';
import CostMonitor from './CostMonitor.js';
import CostDashboard from './CostDashboard.js';
import { getApiUrl } from '../utils/apiConfig.js';
// import {getWarningColor, formatTokens, formatPercentage} from '../utils/formatters.js';
import * as commandProcessor from '../handlers/commandProcessor.js';
import { useAuth } from '../hooks/useAuth.js';
const Terminal = ({ autoLogin, onTriggerRegister, onShowInstructions }) => {
const { auth, login, logout, addConversationEntry, updateContextInfo, formatTokenCount, calculateContextWarning, persistConversation, createNewConversation,
// syncContextInfo, debugContextState
} = useAuth();
const { userId, sessionToken, userName, apiKey, conversationHistory, contextInfo } = auth;
// State for UI
const [history, setHistory] = useState([]);
const [showHistory, setShowHistory] = useState(false);
// ------------------------------------------------------------------------------------------------
// LOADING STATES
const [isLoadingGame, setIsLoadingGame] = useState(false);
const [isProcessingCommand, setIsProcessingCommand] = useState(false);
const [loadingMessage, setLoadingMessage] = useState('');
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// UI STATES
// ------------------------------------------------------------------------------------------------
const [showModelSelector, setShowModelSelector] = useState(false);
const [showLeaderboard, setShowLeaderboard] = useState(false);
const [leaderboardData, setLeaderboardData] = useState(null);
const [leaderboardKey, setLeaderboardKey] = useState(0);
const [showCostDashboard, setShowCostDashboard] = useState(false);
const [costDashboardData, setCostDashboardData] = useState(null);
const [costDashboardKey, setCostDashboardKey] = useState(0);
const [showCostMonitor, setShowCostMonitor] = useState(false);
const [costMonitorData, setCostMonitorData] = useState(null);
const [costMonitorKey, setCostMonitorKey] = useState(0);
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']);
const historyRef = useRef(null);
// const toggleReasoningItem = (index: number) => {
// // const toggleReasoningItem = (index: number) => {
// setHistory((prev: any[]) =>
// prev.map((item, idx) => {
// if (idx !== index || !item?.data) {
// return item;
// }
// return {
// ...item,
// data: {
// ...item.data,
// reasoningExpanded: !item.data.reasoningExpanded,
// },
// };
// })
// );
// };
// Handle ESC key to close overlays
useInput((_, key) => {
if (key.escape) {
if (showHistory) {
setShowHistory(false);
}
else if (showLeaderboard) {
setShowLeaderboard(false);
setLeaderboardData(null);
setLeaderboardKey(prev => prev + 1);
}
else if (showCostDashboard) {
setShowCostDashboard(false);
setCostDashboardData(null);
setCostDashboardKey(prev => prev + 1);
}
else if (showCostMonitor) {
setShowCostMonitor(false);
setCostMonitorData(null);
setCostMonitorKey(prev => prev + 1);
}
else if (showModelSelector) {
setShowModelSelector(false);
}
}
});
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('');
}
};
// MCP functionality has been removed
// ------------------------------------------------------------------------------------------------
// ================================================================================================================================
// MAIN COMMAND HANDLER
// ================================================================================================================================
async function handleCommand(commandValue) {
if (showModelSelector || showLeaderboard)
return;
setHistory(prev => [...prev, { type: 'command', text: commandValue }]);
if (commandValue !== '/history')
setShowHistory(false);
if (commandValue === '/register') {
onTriggerRegister();
return;
}
{
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 || 'unknown');
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,
},
// terminal UI
setHistory,
setIsLoading: setIsProcessingCommand,
setLoadingMessage,
setShowHistory,
setShowModelSelector,
setShowInstructions: onShowInstructions,
setShowLeaderboard: (data) => {
setLeaderboardData(data);
setShowLeaderboard(true);
},
setShowCostDashboard: (data) => {
if (data)
setCostDashboardData(data);
setShowCostDashboard(true);
},
setShowCostMonitor: (data) => {
if (data)
setCostMonitorData(data);
setShowCostMonitor(true);
},
selectedModel,
// Conversation tracking
conversation: {
addEntry: addConversationEntry,
updateContextInfo: updateContextInfo,
formatTokens: formatTokenCount,
calculateWarning: calculateContextWarning,
currentConversation: conversationHistory,
contextInfo: contextInfo,
persistConversation: async (gameId) => {
if (persistConversation) {
return await persistConversation(gameId);
}
return false;
},
createNewConversation,
},
});
}
}
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { marginY: 1, justifyContent: "space-between", padding: 1, flexShrink: 0, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: userName ? `${userName}'s AI Escape Room` : 'AI Escape Room' }), hasAICapability && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 AI available: ", selectedModel?.label] }) }))] }), _jsx(Box, { flexDirection: "column", alignItems: "flex-end", children: _jsx(Box, { children: _jsx(Text, { color: isLoadingGame || !!sessionToken ? 'green' : 'red', children: isLoadingGame || !!sessionToken ? '● Online' : '◌ Offline' }) }) })] }), _jsx(Box, { padding: 1, flexDirection: "column", flexGrow: 1, minHeight: 0, children: _jsx(ScrollableBox, { ref: historyRef, children: showHistory && conversationHistory ? (_jsx(ConversationHistoryComponent, { conversation: conversationHistory, maxEntries: 50, showTokenUsage: true, showCost: true, showReasoningProcess: false })) : (_jsx(CommandHistory, { history: history, showHistory: showHistory })) }) }), showModelSelector && (_jsx(Box, { flexShrink: 0, children: _jsx(ModelSelector, { onSelect: (model) => {
setSelectedModel(model);
setHistory(prev => [
...prev,
{ type: 'response', text: `Model set to ${model.label}` },
]);
}, onClose: () => setShowModelSelector(false) }) })), showLeaderboard && leaderboardData && (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1, flexDirection: "column", children: [_jsx(Leaderboard, { entries: leaderboardData.entries, count: leaderboardData.count, mode: leaderboardData.mode, showCostMetrics: leaderboardData.showCostMetrics }), _jsx(Box, { marginTop: 1, marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: "white", children: "\uD83C\uDFC3\uD83C\uDFFB\u200D\u2642\uFE0F Press ESC to close" }) })] }, `leaderboard-${leaderboardKey}`)), showCostDashboard && (_jsxs(Box, { borderStyle: "round", borderColor: "green", padding: 1, marginBottom: 1, flexDirection: "column", 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, marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: "white", children: "\uD83C\uDFC3\uD83C\uDFFB\u200D\u2642\uFE0F Press ESC to close" }) })] }, `cost-dashboard-${costDashboardKey}`)), showCostMonitor && (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1, flexDirection: "column", 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, marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: "white", children: "\uD83C\uDFC3\uD83C\uDFFB\u200D\u2642\uFE0F Press ESC to close" }) })] }, `cost-monitor-${costMonitorKey}`)), (isProcessingCommand || isLoadingGame) && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, rowGap: 0.5, paddingY: 1, flexShrink: 0, 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." }) })] })), _jsx(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginY: 1, flexShrink: 0, children: _jsx(CommandInput, { value: command, onChange: setCommand, onSubmit: handleCommand, mode: 'standard', currentRoomObjects: currentRoomObjects }) })] }));
};
export default Terminal;