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