learning_ai_invt_trdg/web/src/App.tsx

196 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { useWebSocket } from './hooks/useWebSocket';
import { useAuth } from './components/AuthContext';
import { Login } from './components/Login';
import { ResetPassword } from './components/ResetPassword';
import { ChatControl } from './components/ChatControl';
import { AppContext } from './context/AppContext';
import { AppShell } from './components/layout/AppShell';
import { useBacktestFeatureGate } from './backtest/useBacktestFeatureGate';
import { useTabFeatureFlags } from './hooks/useTabFeatureFlags';
import { tradingRuntime, tradingTelemetry } from './lib/runtime';
import { createTradeProfile, fetchTradeProfiles, updateTradeProfile } from './lib/profileApi';
// ─── Helpers (preserved from original App.tsx) ───────────────────────────────
export const resolveProfileNameForAction = (
action: string,
requestedName: string | undefined,
chatProfiles: Array<{ name?: string }>,
suffixProvider: () => string = () =>
new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '')
) => {
let profileName = requestedName || 'AI Profile';
if (action === 'create_profile') {
const existing = chatProfiles.find(p => p.name === profileName);
if (existing) profileName = `${profileName} (${suffixProvider()})`;
}
return profileName;
};
export const buildChatApplyPayload = (
profileData: any,
currentUserId: string,
profileName: string
) => ({
name: profileName,
user_id: currentUserId,
allocated_capital: Number(profileData.allocated_capital || 1000),
risk_per_trade_percent: Number(profileData.risk_per_trade_percent || 1),
symbols: profileData.symbols || 'BTC/USDT',
is_active: profileData.is_active ?? true,
strategy_config: profileData.strategy_config,
});
// ─── App ─────────────────────────────────────────────────────────────────────
function App() {
const { user, profile, loading, signOut } = useAuth();
const { socket, botState, connected } = useWebSocket(tradingRuntime.tradingApiUrl);
const [activeSymbol, setActiveSymbol] = useState('');
const [chatProfiles, setChatProfiles] = useState<any[]>([]);
const [previewAsCustomer] = useState(false);
const { enabled: backtestEnabledForView, loading: backtestGateLoading } =
useBacktestFeatureGate({ previewAsCustomer });
const { flags: tabFlags } = useTabFeatureFlags();
// Feature gates
const isAdminAccount = profile?.role === 'admin';
const isAdmin = isAdminAccount && !previewAsCustomer;
const showBacktestTab = isAdmin || (!backtestGateLoading && backtestEnabledForView);
const showMarketplaceTab = isAdmin || tabFlags.marketplace;
// Critical system events (for the alert banner)
const recentCriticalEvents = (botState.operationalEvents ?? []).filter(e =>
(e.severity === 'ERROR' || e.severity === 'WARN') &&
Date.now() - e.timestamp < 600_000
);
const hasCriticalEvents = recentCriticalEvents.length > 0;
// Chat profile management
const fetchChatProfiles = useCallback(async () => {
const data = await fetchTradeProfiles();
setChatProfiles(data ?? []);
}, []);
useEffect(() => {
if (user) {
fetchChatProfiles();
const id = setInterval(fetchChatProfiles, 30_000);
return () => clearInterval(id);
}
}, [user, fetchChatProfiles]);
const handleChatApply = async (
action: string,
profileData: any
): Promise<{ success: boolean; error?: string }> => {
const currentUserId = user?.id;
if (!currentUserId) return { success: false, error: 'Not authenticated' };
const profileName = resolveProfileNameForAction(action, profileData.name, chatProfiles);
const payload = buildChatApplyPayload(profileData, currentUserId, profileName);
if (action === 'create_profile') {
try {
await createTradeProfile(payload);
fetchChatProfiles();
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
}
if (action === 'update_profile' && profileData.id) {
try {
await updateTradeProfile(profileData.id, payload);
fetchChatProfiles();
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
}
return { success: false, error: 'Unknown action' };
};
const handleSignOut = async () => {
tradingTelemetry.client.trackEvent('info', 'auth', 'trading_web_sign_out', {
userId: user?.id ?? 'anonymous',
feature: 'sign_out',
tags: { surface: 'web' },
});
await signOut();
};
// ── Auth gates ──────────────────────────────────────────────────────────────
if (window.location.pathname === '/reset-callback') return <ResetPassword />;
if (loading) {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
height: '100vh', background: '#F3F4F6', color: '#374151',
fontSize: 15, fontFamily: 'Inter, system-ui, sans-serif',
}}>
Loading
</div>
);
}
if (!user) return <Login />;
// ── Render ──────────────────────────────────────────────────────────────────
return (
<BrowserRouter>
<AppContext.Provider value={{
botState,
socket,
connected,
activeSymbol,
setActiveSymbol,
isAdmin,
user,
profile,
showBacktestTab,
showMarketplaceTab,
handleSignOut,
}}>
{/* Critical system alert banner */}
{hasCriticalEvents && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
background: 'linear-gradient(90deg, #991b1b 0%, #dc2626 50%, #991b1b 100%)',
color: '#fff',
padding: '6px 20px',
textAlign: 'center',
fontSize: 11,
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '0.1em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
}}>
<span></span>
<span>
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED GO TO SETTINGS ADMIN PANEL
</span>
<span></span>
</div>
)}
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}>
<AppShell />
</div>
{/* Floating AI strategy assistant */}
<ChatControl profiles={chatProfiles} onApplyProfile={handleChatApply} />
</AppContext.Provider>
</BrowserRouter>
);
}
export default App;