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

273 lines (272 loc) 15.9 kB
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;