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

279 lines (278 loc) 11.2 kB
import { jsx as _jsx } from "react/jsx-runtime"; import React, { createContext, useState, useContext } from 'react'; import { formatTokens as sharedFormatTokens, formatCost as sharedFormatCost, calculateContextWarning as sharedCalculateContextWarning } from '../utils/formatters.js'; // ------------------------------------------------------------------------------------------------ export const AuthContext = createContext(undefined); export const AuthProvider = ({ children }) => { const [auth, setAuth] = useState({ costTracking: { currentSessionCost: 0, currentSessionTokens: 0, lastRequestCost: 0, lastRequestTokens: 0, totalCost: 0, totalTokens: 0, } }); // Memoize login to prevent recreating on every render const login = React.useCallback((userData) => { setAuth({ userId: userData.userId, sessionToken: userData.sessionToken, userName: userData.userName, apiKey: userData.apiKey, apiKeyProvider: userData.apiKeyProvider, costTracking: userData.costTracking || { currentSessionCost: 0, currentSessionTokens: 0, lastRequestCost: 0, lastRequestTokens: 0, totalCost: 0, totalTokens: 0, } }); }, []); // Memoize logout to prevent recreating on every render const logout = React.useCallback(() => { setAuth({ costTracking: { currentSessionCost: 0, currentSessionTokens: 0, lastRequestCost: 0, lastRequestTokens: 0, totalCost: 0, totalTokens: 0, } }); }, []); // Memoize updateCostTracking to prevent recreating on every render const updateCostTracking = React.useCallback((costData) => { setAuth(prev => ({ ...prev, costTracking: { ...prev.costTracking, ...costData } })); }, []); // Memoize resetSessionCosts to prevent recreating on every render const resetSessionCosts = React.useCallback(() => { setAuth(prev => ({ ...prev, costTracking: { ...prev.costTracking, currentSessionCost: 0, currentSessionTokens: 0, lastRequestCost: 0, lastRequestTokens: 0, totalCost: 0, totalTokens: 0, budgetAlert: undefined } })); }, []); // Memoize apiCall to prevent recreating on every render const apiCall = React.useCallback(async (endpoint, options = {}) => { const baseUrl = process.env['API_BASE_URL'] || 'http://localhost:3001'; const url = `${baseUrl}${endpoint}`; const headers = { 'Content-Type': 'application/json', ...(auth.sessionToken && { 'Authorization': `Bearer ${auth.sessionToken}` }), ...options.headers }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { throw new Error(`API call failed: ${response.status} ${response.statusText}`); } return response.json(); }, [auth.sessionToken]); // ------------------------------------------------------------------------------------------------ // UTILITY FOR COST AND TOKEN USAGE - Memoized to prevent recreation // Declared before conversation methods since they depend on these // ------------------------------------------------------------------------------------------------ const formatTokenCount = React.useCallback((tokens) => { return sharedFormatTokens(tokens); }, []); const formatCost = React.useCallback((cost) => { return sharedFormatCost(cost); }, []); const calculateContextWarning = React.useCallback((currentSize, maxSize) => { return sharedCalculateContextWarning(currentSize, maxSize); }, []); const calculateContextInfo = React.useCallback((contextStats) => { const warningLevel = calculateContextWarning(contextStats.currentContextSize, contextStats.maxContextSize); return { currentSize: contextStats.currentContextSize, maxSize: contextStats.maxContextSize, percentage: (contextStats.currentContextSize / contextStats.maxContextSize) * 100, warningLevel, formattedSize: sharedFormatTokens(contextStats.currentContextSize), formattedMaxSize: sharedFormatTokens(contextStats.maxContextSize) }; }, [calculateContextWarning]); // ------------------------------------------------------------------------------------------------ // Conversation management methods - Memoized to prevent recreation // ------------------------------------------------------------------------------------------------ const addConversationEntry = React.useCallback(async (entry) => { if (!auth.conversationHistory?.gameId) { console.warn('No active conversation to add entry to'); return; } try { const response = await apiCall(`/api/conversation/${auth.conversationHistory.gameId}/entry`, { method: 'POST', body: JSON.stringify({ type: entry.type, content: entry.content, tokenUsage: entry.tokenUsage, metadata: entry.metadata, reasoningProcess: entry.reasoningProcess }) }); if (response.success) { // Update local conversation history const newEntry = { ...entry, id: `entry_${Date.now()}`, timestamp: new Date().toISOString(), formattedTimestamp: new Date().toLocaleString(), formattedTokens: formatTokenCount(entry.tokenUsage?.totalTokens || 0), formattedCost: formatCost(entry.metadata.cost || 0) }; const updatedContextStats = response.data?.contextStats; let newContextInfo; if (updatedContextStats) { newContextInfo = calculateContextInfo(updatedContextStats); } setAuth(prev => { const updatedConversationHistory = prev.conversationHistory ? { ...prev.conversationHistory, entries: [...prev.conversationHistory.entries, newEntry], contextStats: updatedContextStats || prev.conversationHistory.contextStats, updatedAt: new Date().toISOString() } : undefined; // Always ensure context info is synchronized const finalContextInfo = newContextInfo || (updatedConversationHistory ? calculateContextInfo(updatedConversationHistory.contextStats) : prev.contextInfo); return { ...prev, conversationHistory: updatedConversationHistory, contextInfo: finalContextInfo }; }); } } catch (error) { console.error('Failed to add conversation entry:', error); } }, [apiCall, auth.conversationHistory, formatTokenCount, formatCost, calculateContextInfo]); const updateContextInfo = React.useCallback((contextInfo) => { setAuth(prev => ({ ...prev, contextInfo })); }, []); const getFormattedConversation = React.useCallback(async (gameId) => { try { const response = await apiCall(`/api/conversation/${gameId}/formatted?includeTokenUsage=true&includeCost=true&includeReasoningProcess=true`); if (response.success) { return response.data.conversation; } return null; } catch (error) { console.error('Failed to get formatted conversation:', error); return null; } }, [apiCall]); const persistConversation = React.useCallback(async (gameId) => { try { const response = await apiCall(`/api/conversation/${gameId}/persist`, { method: 'POST' }); return response.success || false; } catch (error) { console.error('Failed to persist conversation:', error); return false; } }, [apiCall]); const createNewConversation = React.useCallback((gameId, sessionId, gameMode) => { const newConversation = { gameId, userId: auth.userId || 'anonymous', sessionId, entries: [], contextStats: { totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0, totalReasoningTokens: 0, totalCost: 0, entryCount: 0, maxContextSize: 128000, // Default context size currentContextSize: 0 }, gameMode, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const initialContextInfo = calculateContextInfo(newConversation.contextStats); setAuth(prev => ({ ...prev, conversationHistory: newConversation, contextInfo: initialContextInfo })); }, [auth.userId, calculateContextInfo]); // Memoize the context value to prevent unnecessary rerenders const contextValue = React.useMemo(() => ({ auth, login, logout, apiCall, updateCostTracking, resetSessionCosts, // Conversation methods addConversationEntry, updateContextInfo, getFormattedConversation, persistConversation, formatTokenCount, calculateContextWarning, createNewConversation, }), [ // Destructure auth to track specific changes rather than entire object auth.userId, auth.sessionToken, auth.userName, auth.apiKey, auth.apiKeyProvider, auth.conversationHistory, auth.contextInfo, auth.costTracking, // All memoized functions login, logout, apiCall, updateCostTracking, resetSessionCosts, addConversationEntry, updateContextInfo, getFormattedConversation, persistConversation, formatTokenCount, calculateContextWarning, createNewConversation, ]); return (_jsx(AuthContext.Provider, { value: contextValue, children: children })); }; export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; };